project-mgr 0.1.7 → 0.2.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.
- package/.claude/settings.local.json +5 -0
- package/.env_sample +3 -0
- package/CHANGELOG.md +12 -0
- package/README.md +31 -3
- package/README_CN.md +43 -15
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +27 -6
- package/dist/db.d.ts +1 -0
- package/dist/db.js +26 -7
- package/dist/domain.js +10 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +147 -0
- package/dist/web/ui.d.ts +1 -0
- package/dist/web/ui.js +977 -0
- package/ecosystem.config.cjs +21 -0
- package/package.json +1 -1
- package/skills/project-mgr/SKILL.md +17 -5
- package/src/cli.ts +32 -6
- package/src/db.ts +31 -10
- package/src/domain.ts +11 -2
- package/src/index.ts +3 -0
- package/src/server.ts +202 -0
- package/src/web/ui.ts +977 -0
- package/test/core.test.ts +46 -7
- package/test/server.test.ts +354 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
apps: [
|
|
3
|
+
{
|
|
4
|
+
name: "project-mgr-web",
|
|
5
|
+
cwd: __dirname,
|
|
6
|
+
script: process.execPath,
|
|
7
|
+
interpreter: "none",
|
|
8
|
+
args: [
|
|
9
|
+
"--env-file-if-exists=.env",
|
|
10
|
+
"dist/cli.js",
|
|
11
|
+
"serve",
|
|
12
|
+
"--host",
|
|
13
|
+
"127.0.0.1",
|
|
14
|
+
"--no-open",
|
|
15
|
+
],
|
|
16
|
+
env: {
|
|
17
|
+
PMGR_DB_PATH: `${process.env.HOME}/.local/share/project-mgr/project-mgr.sqlite`,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: project-mgr
|
|
3
|
-
description: Manage project status with the local-first project-mgr CLI. Use when an AI agent needs to add an append-only project update, set project descriptions/notes, list or fuzzy-search projects, import status records, query current or historical project state, or export a status ledger.
|
|
3
|
+
description: Manage project status with the local-first project-mgr CLI. Use when an AI agent needs to add an append-only project update, set project descriptions/notes, list or fuzzy-search projects, start the local Web Dashboard, import status records, query current or historical project state, or export a status ledger.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# project-mgr
|
|
@@ -13,6 +13,16 @@ Use `npx --yes --package project-mgr pmgr`. In a source checkout, use `pnpm pmgr
|
|
|
13
13
|
- For exploratory or test work, set `PMGR_DB_PATH` to a dedicated absolute SQLite path for every command. Do not use `:memory:` when more than one CLI invocation must share data.
|
|
14
14
|
- Query before writing. `add` appends a snapshot; it does not edit the previous one.
|
|
15
15
|
- Set `--source agent` for agent-authored updates. Use `--external-ref` only when a stable idempotency key is available.
|
|
16
|
+
- If the project is not yet in the ledger database, initialize its metadata with `set-project`, backfill 3–5 historical major changes/milestones using `add` (setting appropriate historical `--at` timestamps), and then append the latest status snapshot.
|
|
17
|
+
|
|
18
|
+
## Metadata vs. status entries
|
|
19
|
+
|
|
20
|
+
- **Project notes (`--notes`)**: Reserved for persistent, medium-to-long-term project context (e.g., architectural constraints, permanent tech stack choices, long-term goals, or critical environment caveats).
|
|
21
|
+
- Manage notes via `set-project --project <name> --notes "..."`.
|
|
22
|
+
- **Do NOT** put transient progress, session logs, or one-off status messages into `--notes`.
|
|
23
|
+
- Avoid passing `--notes` during routine `add` commands to prevent accidentally overwriting long-term project notes.
|
|
24
|
+
- **Project description (`--description`)**: A high-level, stable summary of the project's purpose.
|
|
25
|
+
- **Status snapshots (`add`)**: Use `--summary` (concise milestone headline), `--progress` (detailed work accomplished), `--next` (immediate next steps), `--blocker` (current obstacles), and `--decision` for transactional, append-only updates.
|
|
16
26
|
|
|
17
27
|
## Operations
|
|
18
28
|
|
|
@@ -23,14 +33,16 @@ Prefer `--json` for data that must be interpreted or reported programmatically.
|
|
|
23
33
|
npx --yes --package project-mgr pmgr set-project --project my-project --description "Project summary" --notes "Internal notes" --json
|
|
24
34
|
npx --yes --package project-mgr pmgr set-project --project my-project --clear-description --clear-notes --json
|
|
25
35
|
|
|
26
|
-
|
|
27
36
|
# List all projects or fuzzy search across project names, descriptions, notes, status summaries, and tags.
|
|
28
37
|
npx --yes --package project-mgr pmgr projects --json
|
|
29
38
|
npx --yes --package project-mgr pmgr list --query "keyword" --json
|
|
30
39
|
|
|
31
|
-
#
|
|
32
|
-
npx --yes --package project-mgr pmgr
|
|
33
|
-
|
|
40
|
+
# Start the local Web Dashboard & REST API server.
|
|
41
|
+
npx --yes --package project-mgr pmgr serve --port 3000 --no-open --json
|
|
42
|
+
|
|
43
|
+
# Add an agent-authored status snapshot (status defaults to completed if omitted).
|
|
44
|
+
npx --yes --package project-mgr pmgr add --project my-project --summary "Implemented the CLI" \
|
|
45
|
+
--progress "Tests pass" --next "Publish the package" --source agent --json
|
|
34
46
|
|
|
35
47
|
# Query current state, history, or state at a time.
|
|
36
48
|
npx --yes --package project-mgr pmgr current --project my-project --json
|
package/src/cli.ts
CHANGED
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
openLedger,
|
|
13
13
|
parseTime,
|
|
14
14
|
type ProjectOverview,
|
|
15
|
+
type ProjectStatus,
|
|
15
16
|
setProjectMetadata,
|
|
17
|
+
startServer,
|
|
16
18
|
toPortableStatusEntry,
|
|
17
19
|
ValidationError,
|
|
18
20
|
} from "./index.js";
|
|
@@ -33,7 +35,9 @@ function parse(argv: string[]): { command: string[]; flags: Flags } {
|
|
|
33
35
|
if (
|
|
34
36
|
key === "json" ||
|
|
35
37
|
key === "clear-description" ||
|
|
36
|
-
key === "clear-notes"
|
|
38
|
+
key === "clear-notes" ||
|
|
39
|
+
key === "open" ||
|
|
40
|
+
key === "no-open"
|
|
37
41
|
) {
|
|
38
42
|
if (flags[key] !== undefined)
|
|
39
43
|
throw new ValidationError(`--${key} may only be supplied once`);
|
|
@@ -94,6 +98,7 @@ const allowedFlags = new Map<string, ReadonlySet<string>>([
|
|
|
94
98
|
],
|
|
95
99
|
["at", new Set(["project", "at", "json"])],
|
|
96
100
|
["export", new Set(["project", "format"])],
|
|
101
|
+
["serve", new Set(["port", "host", "open", "no-open", "json"])],
|
|
97
102
|
]);
|
|
98
103
|
|
|
99
104
|
function validateInvocation(command: string[], flags: Flags): string {
|
|
@@ -103,7 +108,7 @@ function validateInvocation(command: string[], flags: Flags): string {
|
|
|
103
108
|
typeof action === "string" ? allowedFlags.get(action) : undefined;
|
|
104
109
|
if (typeof action !== "string" || !allowed)
|
|
105
110
|
throw new ValidationError(
|
|
106
|
-
"usage: pmgr <add|import|current|projects|list|set-project|history|at|export> (or: pmgr status <action>)",
|
|
111
|
+
"usage: pmgr <add|import|current|projects|list|set-project|history|at|export|serve> (or: pmgr status <action>)",
|
|
107
112
|
);
|
|
108
113
|
const positional = command.slice(statusCommand ? 2 : 1);
|
|
109
114
|
if (action === "import") {
|
|
@@ -212,7 +217,7 @@ function imported(file: string): unknown[] {
|
|
|
212
217
|
export function run(
|
|
213
218
|
argv: string[],
|
|
214
219
|
write = process.stdout.write.bind(process.stdout),
|
|
215
|
-
):
|
|
220
|
+
): unknown {
|
|
216
221
|
const { command, flags } = parse(argv);
|
|
217
222
|
const action = validateInvocation(command, flags);
|
|
218
223
|
const db = openLedger();
|
|
@@ -229,7 +234,7 @@ export function run(
|
|
|
229
234
|
output(
|
|
230
235
|
addStatus(db, {
|
|
231
236
|
project,
|
|
232
|
-
status: stringFlag(flags, "status"
|
|
237
|
+
status: stringFlag(flags, "status") as unknown as ProjectStatus,
|
|
233
238
|
summary: stringFlag(flags, "summary", true),
|
|
234
239
|
progress: stringFlag(flags, "progress"),
|
|
235
240
|
blockers: listFlag(flags, "blocker"),
|
|
@@ -352,9 +357,30 @@ export function run(
|
|
|
352
357
|
else throw new ValidationError("--format must be json or jsonl");
|
|
353
358
|
break;
|
|
354
359
|
}
|
|
360
|
+
|
|
361
|
+
case "serve": {
|
|
362
|
+
const port =
|
|
363
|
+
flags.port !== undefined
|
|
364
|
+
? numberFlag(flags, "port", 3000)
|
|
365
|
+
: undefined;
|
|
366
|
+
const host = stringFlag(flags, "host");
|
|
367
|
+
const open = flags["no-open"] === true ? false : true;
|
|
368
|
+
return startServer({ port, host, open, db }).then((instance) => {
|
|
369
|
+
if (json) {
|
|
370
|
+
write(
|
|
371
|
+
`${JSON.stringify({ url: instance.url, port: instance.port, host: instance.host })}\n`,
|
|
372
|
+
);
|
|
373
|
+
} else {
|
|
374
|
+
write(`Server running at ${instance.url}\n`);
|
|
375
|
+
}
|
|
376
|
+
return instance;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
355
379
|
}
|
|
356
380
|
} finally {
|
|
357
|
-
|
|
381
|
+
if (action !== "serve") {
|
|
382
|
+
db.close();
|
|
383
|
+
}
|
|
358
384
|
}
|
|
359
385
|
}
|
|
360
386
|
|
|
@@ -364,7 +390,7 @@ if (
|
|
|
364
390
|
import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href
|
|
365
391
|
) {
|
|
366
392
|
try {
|
|
367
|
-
run(process.argv.slice(2));
|
|
393
|
+
await run(process.argv.slice(2));
|
|
368
394
|
} catch (error) {
|
|
369
395
|
console.error(error instanceof Error ? error.message : String(error));
|
|
370
396
|
process.exitCode = 1;
|
package/src/db.ts
CHANGED
|
@@ -207,6 +207,15 @@ export function listProjects(
|
|
|
207
207
|
};
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
+
overviews.sort((a, b) => {
|
|
211
|
+
const timeA = a.latestStatus?.occurredAt ?? a.createdAt;
|
|
212
|
+
const timeB = b.latestStatus?.occurredAt ?? b.createdAt;
|
|
213
|
+
if (timeA !== timeB) {
|
|
214
|
+
return timeB.localeCompare(timeA);
|
|
215
|
+
}
|
|
216
|
+
return a.project.localeCompare(b.project);
|
|
217
|
+
});
|
|
218
|
+
|
|
210
219
|
if (!query) return overviews;
|
|
211
220
|
|
|
212
221
|
return overviews.filter((overview) => {
|
|
@@ -215,16 +224,13 @@ export function listProjects(
|
|
|
215
224
|
if (overview.description?.toLowerCase().includes(query)) return true;
|
|
216
225
|
if (overview.notes?.toLowerCase().includes(query)) return true;
|
|
217
226
|
if (overview.latestStatus) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
overview.latestStatus.tags.some((t) => t.toLowerCase().includes(query))
|
|
226
|
-
)
|
|
227
|
-
return true;
|
|
227
|
+
const st = overview.latestStatus;
|
|
228
|
+
const match =
|
|
229
|
+
st.status.toLowerCase().includes(query) ||
|
|
230
|
+
st.summary.toLowerCase().includes(query) ||
|
|
231
|
+
(st.progress && st.progress.toLowerCase().includes(query)) ||
|
|
232
|
+
st.tags.some((t) => t.toLowerCase().includes(query));
|
|
233
|
+
if (match) return true;
|
|
228
234
|
}
|
|
229
235
|
return false;
|
|
230
236
|
});
|
|
@@ -350,3 +356,18 @@ export function at(
|
|
|
350
356
|
occurredAt,
|
|
351
357
|
);
|
|
352
358
|
}
|
|
359
|
+
|
|
360
|
+
export function countEntries(db: Ledger, project?: string): number {
|
|
361
|
+
if (project) {
|
|
362
|
+
const row = db
|
|
363
|
+
.prepare(
|
|
364
|
+
"SELECT COUNT(*) AS count FROM status_entries e JOIN projects p ON p.id = e.project_id WHERE p.slug = ?",
|
|
365
|
+
)
|
|
366
|
+
.get(project) as { count: number };
|
|
367
|
+
return row.count;
|
|
368
|
+
}
|
|
369
|
+
const row = db
|
|
370
|
+
.prepare("SELECT COUNT(*) AS count FROM status_entries")
|
|
371
|
+
.get() as { count: number };
|
|
372
|
+
return row.count;
|
|
373
|
+
}
|
package/src/domain.ts
CHANGED
|
@@ -173,7 +173,16 @@ export function validateStatusEntry(
|
|
|
173
173
|
throw new ValidationError(
|
|
174
174
|
"project must use letters, numbers, dots, underscores, or hyphens",
|
|
175
175
|
);
|
|
176
|
-
|
|
176
|
+
let statusInput = input.status;
|
|
177
|
+
if (statusInput === undefined || statusInput === null) {
|
|
178
|
+
statusInput = "completed";
|
|
179
|
+
} else if (
|
|
180
|
+
typeof statusInput === "string" &&
|
|
181
|
+
statusInput.trim().toLowerCase() === "done"
|
|
182
|
+
) {
|
|
183
|
+
statusInput = "completed";
|
|
184
|
+
}
|
|
185
|
+
if (!statuses.includes(statusInput as ProjectStatus))
|
|
177
186
|
throw new ValidationError("status is invalid");
|
|
178
187
|
const source = input.source === undefined ? defaultSource : input.source;
|
|
179
188
|
if (!sources.includes(source as Source))
|
|
@@ -183,7 +192,7 @@ export function validateStatusEntry(
|
|
|
183
192
|
throw new ValidationError("externalRef must be at most 200 characters");
|
|
184
193
|
return {
|
|
185
194
|
project,
|
|
186
|
-
status:
|
|
195
|
+
status: statusInput as ProjectStatus,
|
|
187
196
|
summary: text(input.summary, "summary", true) as string,
|
|
188
197
|
progress: text(input.progress, "progress"),
|
|
189
198
|
blockers: textList(input.blockers, "blockers"),
|
package/src/index.ts
CHANGED
package/src/server.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
createServer,
|
|
4
|
+
type IncomingMessage,
|
|
5
|
+
type Server,
|
|
6
|
+
type ServerResponse,
|
|
7
|
+
} from "node:http";
|
|
8
|
+
import { URL } from "node:url";
|
|
9
|
+
import {
|
|
10
|
+
countEntries,
|
|
11
|
+
history,
|
|
12
|
+
listProjects,
|
|
13
|
+
openLedger,
|
|
14
|
+
type Ledger,
|
|
15
|
+
} from "./db.js";
|
|
16
|
+
import { getWebUIHTML } from "./web/ui.js";
|
|
17
|
+
|
|
18
|
+
export type ServerOptions = {
|
|
19
|
+
port?: number;
|
|
20
|
+
host?: string;
|
|
21
|
+
open?: boolean;
|
|
22
|
+
db?: Ledger;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ServerInstance = {
|
|
26
|
+
server: Server;
|
|
27
|
+
url: string;
|
|
28
|
+
port: number;
|
|
29
|
+
host: string;
|
|
30
|
+
close: () => Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function openBrowser(url: string): void {
|
|
34
|
+
const platform = process.platform;
|
|
35
|
+
let command = "";
|
|
36
|
+
if (platform === "darwin") {
|
|
37
|
+
command = `open "${url}"`;
|
|
38
|
+
} else if (platform === "win32") {
|
|
39
|
+
command = `start "" "${url}"`;
|
|
40
|
+
} else {
|
|
41
|
+
command = `xdg-open "${url}"`;
|
|
42
|
+
}
|
|
43
|
+
exec(command, () => {
|
|
44
|
+
/* ignore background exec errors */
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseQueryParams(url: URL): Record<string, string> {
|
|
49
|
+
const params: Record<string, string> = {};
|
|
50
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
51
|
+
params[key] = value;
|
|
52
|
+
}
|
|
53
|
+
return params;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sendJson(
|
|
57
|
+
res: ServerResponse,
|
|
58
|
+
statusCode: number,
|
|
59
|
+
data: unknown,
|
|
60
|
+
): void {
|
|
61
|
+
res.writeHead(statusCode, {
|
|
62
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
63
|
+
"Cache-Control": "no-cache",
|
|
64
|
+
});
|
|
65
|
+
res.end(JSON.stringify(data));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function handleRequest(
|
|
69
|
+
req: IncomingMessage,
|
|
70
|
+
res: ServerResponse,
|
|
71
|
+
db: Ledger,
|
|
72
|
+
): void {
|
|
73
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
74
|
+
sendJson(res, 405, { error: "Method not allowed" });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const reqUrl = new URL(
|
|
79
|
+
req.url || "/",
|
|
80
|
+
`http://${req.headers.host || "localhost"}`,
|
|
81
|
+
);
|
|
82
|
+
const pathname = reqUrl.pathname;
|
|
83
|
+
const query = parseQueryParams(reqUrl);
|
|
84
|
+
|
|
85
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
86
|
+
res.writeHead(200, {
|
|
87
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
88
|
+
"Cache-Control": "no-cache",
|
|
89
|
+
});
|
|
90
|
+
res.end(getWebUIHTML());
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (pathname === "/api/projects") {
|
|
95
|
+
const projects = listProjects(db, { query: query.query });
|
|
96
|
+
sendJson(res, 200, projects);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (pathname.startsWith("/api/projects/")) {
|
|
101
|
+
const slug = decodeURIComponent(pathname.slice("/api/projects/".length));
|
|
102
|
+
const projects = listProjects(db);
|
|
103
|
+
const target = projects.find((p) => p.project === slug);
|
|
104
|
+
if (!target) {
|
|
105
|
+
sendJson(res, 404, { error: "Project not found" });
|
|
106
|
+
} else {
|
|
107
|
+
sendJson(res, 200, target);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (pathname === "/api/history") {
|
|
113
|
+
const project = query.project;
|
|
114
|
+
const since = query.since;
|
|
115
|
+
const until = query.until;
|
|
116
|
+
const rawLimit = Number(query.limit ?? 100);
|
|
117
|
+
const rawOffset = Number(query.offset ?? 0);
|
|
118
|
+
|
|
119
|
+
const entries = history(db, project, {
|
|
120
|
+
since,
|
|
121
|
+
until,
|
|
122
|
+
limit: Number.isInteger(rawLimit) && rawLimit >= 0 ? rawLimit : 100,
|
|
123
|
+
offset: Number.isInteger(rawOffset) && rawOffset >= 0 ? rawOffset : 0,
|
|
124
|
+
});
|
|
125
|
+
sendJson(res, 200, entries);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (pathname === "/api/stats") {
|
|
130
|
+
const projects = listProjects(db);
|
|
131
|
+
const totalEntries = countEntries(db);
|
|
132
|
+
const statusCounts: Record<string, number> = {};
|
|
133
|
+
|
|
134
|
+
for (const proj of projects) {
|
|
135
|
+
const st = proj.latestStatus?.status || "none";
|
|
136
|
+
statusCounts[st] = (statusCounts[st] || 0) + 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
sendJson(res, 200, {
|
|
140
|
+
totalProjects: projects.length,
|
|
141
|
+
totalEntries,
|
|
142
|
+
statusCounts,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
sendJson(res, 404, { error: "Not found" });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function startServer(
|
|
151
|
+
options: ServerOptions = {},
|
|
152
|
+
): Promise<ServerInstance> {
|
|
153
|
+
const host = options.host ?? "127.0.0.1";
|
|
154
|
+
const port =
|
|
155
|
+
options.port ??
|
|
156
|
+
(process.env.PORT ? Number(process.env.PORT) : /* c8 ignore next */ 3000);
|
|
157
|
+
const db = options.db || openLedger();
|
|
158
|
+
const shouldOpen = options.open ?? true;
|
|
159
|
+
|
|
160
|
+
const server = createServer((req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
handleRequest(req, res, db);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
sendJson(res, 500, {
|
|
165
|
+
error: err instanceof Error ? err.message : String(err),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
server.once("error", reject);
|
|
172
|
+
server.listen(port, host, () => {
|
|
173
|
+
/* c8 ignore start -- server.address() ternary branch is Node network adapter glue */
|
|
174
|
+
const address = server.address();
|
|
175
|
+
const actualPort =
|
|
176
|
+
typeof address === "object" && address !== null ? address.port : port;
|
|
177
|
+
/* c8 ignore stop */
|
|
178
|
+
const url = `http://${host}:${actualPort}`;
|
|
179
|
+
|
|
180
|
+
if (shouldOpen) {
|
|
181
|
+
openBrowser(url);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
resolve({
|
|
185
|
+
server,
|
|
186
|
+
url,
|
|
187
|
+
port: actualPort,
|
|
188
|
+
host,
|
|
189
|
+
close: () =>
|
|
190
|
+
new Promise<void>((resClose, rejClose) => {
|
|
191
|
+
server.close((err) => {
|
|
192
|
+
if (!options.db) {
|
|
193
|
+
db.close();
|
|
194
|
+
}
|
|
195
|
+
if (err) rejClose(err);
|
|
196
|
+
else resClose();
|
|
197
|
+
});
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|