nomoreide 0.1.98 → 0.1.100
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/README.md +26 -1
- package/dist/cli/git.js +5 -1
- package/dist/cli/git.js.map +1 -1
- package/dist/cli/profile.js +1 -1
- package/dist/cli/profile.js.map +1 -1
- package/dist/core/agent-profiles/index.d.ts +1 -1
- package/dist/core/agent-profiles/index.js +1 -1
- package/dist/core/agent-profiles/index.js.map +1 -1
- package/dist/core/agent-profiles/registry-client.d.ts +2 -1
- package/dist/core/agent-profiles/registry-client.js.map +1 -1
- package/dist/core/agent-profiles/registry-config.d.ts +16 -2
- package/dist/core/agent-profiles/registry-config.js +76 -22
- package/dist/core/agent-profiles/registry-config.js.map +1 -1
- package/dist/core/agent-profiles/registry-transfer.js +15 -1
- package/dist/core/agent-profiles/registry-transfer.js.map +1 -1
- package/dist/core/agent-profiles/store.d.ts +10 -0
- package/dist/core/agent-profiles/store.js +47 -1
- package/dist/core/agent-profiles/store.js.map +1 -1
- package/dist/core/agent-profiles/transfer.js +28 -5
- package/dist/core/agent-profiles/transfer.js.map +1 -1
- package/dist/core/agent-terminal.d.ts +3 -1
- package/dist/core/agent-terminal.js +24 -5
- package/dist/core/agent-terminal.js.map +1 -1
- package/dist/core/agent-transcripts.js +36 -3
- package/dist/core/agent-transcripts.js.map +1 -1
- package/dist/core/app-settings.d.ts +6 -0
- package/dist/core/app-settings.js +6 -0
- package/dist/core/app-settings.js.map +1 -1
- package/dist/core/config-store.d.ts +35 -1
- package/dist/core/config-store.js +173 -0
- package/dist/core/config-store.js.map +1 -1
- package/dist/core/db/data-masking.d.ts +3 -0
- package/dist/core/db/data-masking.js +14 -0
- package/dist/core/db/data-masking.js.map +1 -0
- package/dist/core/db/driver.d.ts +29 -1
- package/dist/core/db/driver.js +94 -0
- package/dist/core/db/driver.js.map +1 -1
- package/dist/core/db/export.d.ts +17 -0
- package/dist/core/db/export.js +67 -0
- package/dist/core/db/export.js.map +1 -0
- package/dist/core/db/mysql-driver.d.ts +3 -2
- package/dist/core/db/mysql-driver.js +53 -3
- package/dist/core/db/mysql-driver.js.map +1 -1
- package/dist/core/db/postgres-driver.d.ts +3 -2
- package/dist/core/db/postgres-driver.js +41 -3
- package/dist/core/db/postgres-driver.js.map +1 -1
- package/dist/core/db/sqlite-driver.d.ts +3 -2
- package/dist/core/db/sqlite-driver.js +38 -4
- package/dist/core/db/sqlite-driver.js.map +1 -1
- package/dist/core/db-peek.d.ts +9 -2
- package/dist/core/db-peek.js +20 -10
- package/dist/core/db-peek.js.map +1 -1
- package/dist/core/git-actions.d.ts +19 -0
- package/dist/core/git-actions.js +36 -4
- package/dist/core/git-actions.js.map +1 -1
- package/dist/core/git-identity.d.ts +78 -0
- package/dist/core/git-identity.js +178 -0
- package/dist/core/git-identity.js.map +1 -0
- package/dist/core/git-manager.d.ts +9 -1
- package/dist/core/git-manager.js +10 -3
- package/dist/core/git-manager.js.map +1 -1
- package/dist/core/github-context.js +1 -29
- package/dist/core/github-context.js.map +1 -1
- package/dist/core/github-manager.d.ts +6 -0
- package/dist/core/github-manager.js.map +1 -1
- package/dist/core/process-manager.d.ts +14 -0
- package/dist/core/process-manager.js +49 -19
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/project-overview.d.ts +52 -0
- package/dist/core/project-overview.js +122 -0
- package/dist/core/project-overview.js.map +1 -0
- package/dist/core/repo-match.d.ts +12 -0
- package/dist/core/repo-match.js +40 -0
- package/dist/core/repo-match.js.map +1 -0
- package/dist/core/repo-onboard.d.ts +4 -4
- package/dist/core/service-detect.d.ts +19 -0
- package/dist/core/service-detect.js +141 -0
- package/dist/core/service-detect.js.map +1 -0
- package/dist/core/ssh-servers.d.ts +52 -0
- package/dist/core/ssh-servers.js +194 -0
- package/dist/core/ssh-servers.js.map +1 -0
- package/dist/core/terminal-session.d.ts +8 -0
- package/dist/core/terminal-session.js +20 -0
- package/dist/core/terminal-session.js.map +1 -1
- package/dist/core/types.d.ts +62 -0
- package/dist/core/vercel-actions.d.ts +71 -0
- package/dist/core/vercel-actions.js +109 -0
- package/dist/core/vercel-actions.js.map +1 -0
- package/dist/core/vercel-auth.d.ts +58 -0
- package/dist/core/vercel-auth.js +130 -0
- package/dist/core/vercel-auth.js.map +1 -0
- package/dist/core/vercel-context.d.ts +25 -0
- package/dist/core/vercel-context.js +143 -0
- package/dist/core/vercel-context.js.map +1 -0
- package/dist/core/vercel-manager.d.ts +253 -0
- package/dist/core/vercel-manager.js +391 -0
- package/dist/core/vercel-manager.js.map +1 -0
- package/dist/core/vercel-oauth.d.ts +75 -0
- package/dist/core/vercel-oauth.js +223 -0
- package/dist/core/vercel-oauth.js.map +1 -0
- package/dist/index.js +0 -0
- package/dist/mcp/tools/agent-registry.d.ts +2 -2
- package/dist/mcp/tools/agent-registry.js +3 -3
- package/dist/mcp/tools/agent-registry.js.map +1 -1
- package/dist/mcp/tools/docs.d.ts +1 -1
- package/dist/mcp/tools/docs.js +20 -8
- package/dist/mcp/tools/docs.js.map +1 -1
- package/dist/mcp/tools/git.js +20 -2
- package/dist/mcp/tools/git.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.js +3 -0
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/vercel.d.ts +12 -0
- package/dist/mcp/tools/vercel.js +103 -0
- package/dist/mcp/tools/vercel.js.map +1 -0
- package/dist/mcp/tools.d.ts +19 -0
- package/dist/mcp/tools.js +354 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/web/client/assets/code-editor-DFOGG6ys.js +30 -0
- package/dist/web/client/assets/index-BG_E_64s.css +1 -0
- package/dist/web/client/assets/index-BhDAhmA1.js +1 -0
- package/dist/web/client/assets/index-CYgm7PZW.js +346 -0
- package/dist/web/client/assets/{system-process-table-yI0XrfjG.js → system-process-table-B9uNpXW9.js} +1 -1
- package/dist/web/client/index.html +2 -2
- package/dist/web/routes/agent-chat-routes.js +29 -0
- package/dist/web/routes/agent-chat-routes.js.map +1 -1
- package/dist/web/routes/agent-profile-routes.js +14 -2
- package/dist/web/routes/agent-profile-routes.js.map +1 -1
- package/dist/web/routes/database-routes.js +108 -1
- package/dist/web/routes/database-routes.js.map +1 -1
- package/dist/web/routes/git-routes.js +37 -7
- package/dist/web/routes/git-routes.js.map +1 -1
- package/dist/web/routes/index.js +6 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/overview-routes.d.ts +2 -0
- package/dist/web/routes/overview-routes.js +36 -0
- package/dist/web/routes/overview-routes.js.map +1 -0
- package/dist/web/routes/shell-routes.d.ts +8 -0
- package/dist/web/routes/shell-routes.js +10 -2
- package/dist/web/routes/shell-routes.js.map +1 -1
- package/dist/web/routes/ssh-server-routes.d.ts +3 -0
- package/dist/web/routes/ssh-server-routes.js +101 -0
- package/dist/web/routes/ssh-server-routes.js.map +1 -0
- package/dist/web/routes/terminal-routes.js +18 -4
- package/dist/web/routes/terminal-routes.js.map +1 -1
- package/dist/web/routes/vercel-routes.d.ts +2 -0
- package/dist/web/routes/vercel-routes.js +476 -0
- package/dist/web/routes/vercel-routes.js.map +1 -0
- package/dist/web/server.d.ts +8 -0
- package/dist/web/server.js +25 -7
- package/dist/web/server.js.map +1 -1
- package/dist/web/ui-lifecycle.d.ts +30 -0
- package/dist/web/ui-lifecycle.js +129 -0
- package/dist/web/ui-lifecycle.js.map +1 -0
- package/package.json +4 -1
- package/dist/web/client/assets/code-editor-C9zfB-Qp.js +0 -28
- package/dist/web/client/assets/index-BW5OtB6c.js +0 -308
- package/dist/web/client/assets/index-DofkHtdO.css +0 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project summaries for the all-projects lens.
|
|
3
|
+
*
|
|
4
|
+
* The Git, GitHub, and Vercel pages each read the daemon's *selected*
|
|
5
|
+
* repository, which is global state — so "show me all projects" cannot be
|
|
6
|
+
* served by asking those pages nicely. Everything here resolves a project from
|
|
7
|
+
* its own path instead and never touches `selectedGitRepository`, which is what
|
|
8
|
+
* makes an overview possible without the act of looking changing what the rest
|
|
9
|
+
* of the app is pointed at.
|
|
10
|
+
*
|
|
11
|
+
* One project failing is normal (no remote, no Vercel link, a token that does
|
|
12
|
+
* not cover it), so a failure is carried on that project's own row rather than
|
|
13
|
+
* failing the whole request.
|
|
14
|
+
*/
|
|
15
|
+
import { GitManager } from "./git-manager.js";
|
|
16
|
+
import { optionalGitHubContext } from "./github-context.js";
|
|
17
|
+
import { optionalVercelContext } from "./vercel-context.js";
|
|
18
|
+
/**
|
|
19
|
+
* How many projects are summarized concurrently.
|
|
20
|
+
*
|
|
21
|
+
* Each GitHub row costs two API calls and each Vercel row up to three, so an
|
|
22
|
+
* unbounded fan-out over a large workspace would burst straight into a rate
|
|
23
|
+
* limit. Six keeps a realistic workspace responsive without doing that.
|
|
24
|
+
*/
|
|
25
|
+
const CONCURRENCY = 6;
|
|
26
|
+
export async function buildProjectOverview(configStore, domain) {
|
|
27
|
+
const config = await configStore.load();
|
|
28
|
+
return mapWithLimit(config.gitRepositories, CONCURRENCY, (repository) => summarize(configStore, repository, domain));
|
|
29
|
+
}
|
|
30
|
+
async function summarize(configStore, repository, domain) {
|
|
31
|
+
const cwd = repository.activeWorktreePath ?? repository.path;
|
|
32
|
+
const base = { name: repository.name, path: repository.path, cwd };
|
|
33
|
+
try {
|
|
34
|
+
switch (domain) {
|
|
35
|
+
case "git":
|
|
36
|
+
return { ...base, git: await gitOverview(cwd) };
|
|
37
|
+
case "github":
|
|
38
|
+
return { ...base, github: await githubOverview(configStore, cwd) };
|
|
39
|
+
case "vercel":
|
|
40
|
+
return { ...base, vercel: await vercelOverview(configStore, cwd) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return { ...base, error: error instanceof Error ? error.message : String(error) };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function gitOverview(cwd) {
|
|
48
|
+
const status = await new GitManager(cwd).status();
|
|
49
|
+
return {
|
|
50
|
+
branch: status.branch,
|
|
51
|
+
dirty: status.files.length,
|
|
52
|
+
ahead: status.ahead,
|
|
53
|
+
behind: status.behind,
|
|
54
|
+
upstream: status.upstream,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function githubOverview(configStore, cwd) {
|
|
58
|
+
const context = await optionalGitHubContext(configStore, cwd);
|
|
59
|
+
if (!context)
|
|
60
|
+
throw new Error("No GitHub repository resolves for this project.");
|
|
61
|
+
const prs = await context.manager.listPRs("open");
|
|
62
|
+
// CI is a nice-to-have on a card: a repo with no Actions, or a token without
|
|
63
|
+
// the checks scope, should still show its PR count rather than an error.
|
|
64
|
+
const checks = await context.manager
|
|
65
|
+
.repoInfo()
|
|
66
|
+
.then((info) => context.manager.getCommitChecks(info.default_branch ?? "HEAD"))
|
|
67
|
+
.then((result) => normalizeChecks(result.state))
|
|
68
|
+
.catch(() => undefined);
|
|
69
|
+
return {
|
|
70
|
+
owner: context.owner,
|
|
71
|
+
repo: context.repo,
|
|
72
|
+
openPullRequests: prs.length,
|
|
73
|
+
checks,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** The card only distinguishes three outcomes; anything else reads as unknown. */
|
|
77
|
+
function normalizeChecks(state) {
|
|
78
|
+
if (state === "success")
|
|
79
|
+
return "success";
|
|
80
|
+
if (state === "failure")
|
|
81
|
+
return "failure";
|
|
82
|
+
if (state === "pending" || state === "in_progress" || state === "queued")
|
|
83
|
+
return "pending";
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
async function vercelOverview(configStore, cwd) {
|
|
87
|
+
const context = await optionalVercelContext(configStore, cwd);
|
|
88
|
+
if (!context?.project)
|
|
89
|
+
throw new Error("No Vercel project is linked to this project.");
|
|
90
|
+
// Production only, and only the latest: the card answers "is what users see
|
|
91
|
+
// healthy", not "what has been built lately".
|
|
92
|
+
const [latest] = await context.manager.listDeployments({
|
|
93
|
+
projectId: context.project.id,
|
|
94
|
+
target: "production",
|
|
95
|
+
limit: 1,
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
projectId: context.project.id,
|
|
99
|
+
projectName: context.project.name,
|
|
100
|
+
state: latest?.state,
|
|
101
|
+
url: latest?.url ?? undefined,
|
|
102
|
+
createdAt: latest?.createdAt,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `Promise.all` over a bounded worker pool, preserving input order. Workers
|
|
107
|
+
* pull from a shared cursor, so a slow project delays only itself rather than
|
|
108
|
+
* holding up a whole batch the way chunked `Promise.all` rounds would.
|
|
109
|
+
*/
|
|
110
|
+
async function mapWithLimit(items, limit, map) {
|
|
111
|
+
const results = new Array(items.length);
|
|
112
|
+
let cursor = 0;
|
|
113
|
+
async function worker() {
|
|
114
|
+
while (cursor < items.length) {
|
|
115
|
+
const index = cursor++;
|
|
116
|
+
results[index] = await map(items[index]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=project-overview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-overview.js","sourceRoot":"","sources":["../../src/core/project-overview.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GitRepositoryDefinition, NoMoreIdeConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Which registered repository a working directory belongs to.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately strict: an ambiguous or nested match throws rather than picking
|
|
6
|
+
* one, because the answer decides which GitHub account a push or commit is
|
|
7
|
+
* attributed to — guessing there would be worse than refusing.
|
|
8
|
+
*
|
|
9
|
+
* Lives in its own module so both the GitHub API context and the commit/push
|
|
10
|
+
* identity path share one definition without importing each other.
|
|
11
|
+
*/
|
|
12
|
+
export declare function matchRegisteredRepository(config: NoMoreIdeConfig, topLevel: string): Promise<GitRepositoryDefinition | undefined>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Which registered repository a working directory belongs to.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately strict: an ambiguous or nested match throws rather than picking
|
|
7
|
+
* one, because the answer decides which GitHub account a push or commit is
|
|
8
|
+
* attributed to — guessing there would be worse than refusing.
|
|
9
|
+
*
|
|
10
|
+
* Lives in its own module so both the GitHub API context and the commit/push
|
|
11
|
+
* identity path share one definition without importing each other.
|
|
12
|
+
*/
|
|
13
|
+
export async function matchRegisteredRepository(config, topLevel) {
|
|
14
|
+
const canonicalTopLevel = await canonicalPath(topLevel);
|
|
15
|
+
const candidates = [];
|
|
16
|
+
for (const repository of config.gitRepositories) {
|
|
17
|
+
for (const root of [repository.path, repository.activeWorktreePath]) {
|
|
18
|
+
if (root)
|
|
19
|
+
candidates.push({ repository, root: await canonicalPath(root) });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const exact = candidates.filter((candidate) => candidate.root === canonicalTopLevel);
|
|
23
|
+
const names = new Set(exact.map((candidate) => candidate.repository.name));
|
|
24
|
+
if (names.size === 1)
|
|
25
|
+
return exact[0]?.repository;
|
|
26
|
+
if (names.size > 1)
|
|
27
|
+
throw new Error("The Git repository matches multiple registered projects.");
|
|
28
|
+
if (candidates.some((candidate) => pathContains(candidate.root, canonicalTopLevel))) {
|
|
29
|
+
throw new Error("This nested Git repository is not registered with its own GitHub account.");
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
async function canonicalPath(path) {
|
|
34
|
+
return realpath(path).catch(() => resolve(path));
|
|
35
|
+
}
|
|
36
|
+
function pathContains(root, candidate) {
|
|
37
|
+
const child = relative(root, candidate);
|
|
38
|
+
return child === "" || (!child.startsWith("..") && !isAbsolute(child));
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=repo-match.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-match.js","sourceRoot":"","sources":["../../src/core/repo-match.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG1D;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,MAAuB,EACvB,QAAgB;IAEhB,MAAM,iBAAiB,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IACxD,MAAM,UAAU,GAAiE,EAAE,CAAC;IACpF,KAAK,MAAM,UAAU,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;QAChD,KAAK,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;YACpE,IAAI,IAAI;gBAAE,UAAU,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,iBAAiB,CAAC,CAAC;IACrF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3E,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC;IAClD,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAChG,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC;QACpF,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC/F,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAY;IACvC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,SAAiB;IACnD,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACxC,OAAO,KAAK,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;AACzE,CAAC"}
|
|
@@ -217,9 +217,9 @@ export declare const proposedServiceSchema: z.ZodObject<{
|
|
|
217
217
|
}, "strip", z.ZodTypeAny, {
|
|
218
218
|
kind: "local" | "docker-compose";
|
|
219
219
|
name: string;
|
|
220
|
+
reason: string;
|
|
220
221
|
cwd: string;
|
|
221
222
|
confidence: "high" | "medium" | "low";
|
|
222
|
-
reason: string;
|
|
223
223
|
port?: number | undefined;
|
|
224
224
|
command?: string | undefined;
|
|
225
225
|
description?: string | undefined;
|
|
@@ -230,9 +230,9 @@ export declare const proposedServiceSchema: z.ZodObject<{
|
|
|
230
230
|
}, {
|
|
231
231
|
kind: "local" | "docker-compose";
|
|
232
232
|
name: string;
|
|
233
|
+
reason: string;
|
|
233
234
|
cwd: string;
|
|
234
235
|
confidence: "high" | "medium" | "low";
|
|
235
|
-
reason: string;
|
|
236
236
|
port?: number | undefined;
|
|
237
237
|
command?: string | undefined;
|
|
238
238
|
description?: string | undefined;
|
|
@@ -257,17 +257,17 @@ export declare const proposedDatabaseSchema: z.ZodObject<{
|
|
|
257
257
|
reason: z.ZodString;
|
|
258
258
|
}, "strip", z.ZodTypeAny, {
|
|
259
259
|
name: string;
|
|
260
|
+
reason: string;
|
|
260
261
|
engine: "postgres" | "mysql";
|
|
261
262
|
url: string;
|
|
262
263
|
confidence: "high" | "medium" | "low";
|
|
263
|
-
reason: string;
|
|
264
264
|
composeService?: string | undefined;
|
|
265
265
|
}, {
|
|
266
266
|
name: string;
|
|
267
|
+
reason: string;
|
|
267
268
|
engine: "postgres" | "mysql";
|
|
268
269
|
url: string;
|
|
269
270
|
confidence: "high" | "medium" | "low";
|
|
270
|
-
reason: string;
|
|
271
271
|
composeService?: string | undefined;
|
|
272
272
|
}>;
|
|
273
273
|
export type ProposedDatabase = z.infer<typeof proposedDatabaseSchema>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type DetectedKind = "local" | "docker-compose" | "ssh";
|
|
2
|
+
/**
|
|
3
|
+
* A best-guess service definition derived from a folder's contents, used to
|
|
4
|
+
* pre-fill the guided "add service" wizard. Everything is a suggestion the user
|
|
5
|
+
* can override — detection never registers anything on its own.
|
|
6
|
+
*/
|
|
7
|
+
export interface ServiceSuggestion {
|
|
8
|
+
kind: DetectedKind;
|
|
9
|
+
name: string;
|
|
10
|
+
command?: string;
|
|
11
|
+
composeFile?: string;
|
|
12
|
+
composeService?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Human-readable reason ("Found package.json — pnpm dev script"). */
|
|
16
|
+
detected: string;
|
|
17
|
+
}
|
|
18
|
+
/** Inspect a local folder and propose a runnable service definition. */
|
|
19
|
+
export declare function detectServiceSetup(cwd: string): Promise<ServiceSuggestion>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
const PORT_IN_SCRIPT = /(?:--port[=\s]+|-p\s+|PORT=)(\d{2,5})\b/;
|
|
4
|
+
/** Inspect a local folder and propose a runnable service definition. */
|
|
5
|
+
export async function detectServiceSetup(cwd) {
|
|
6
|
+
const root = resolve(cwd);
|
|
7
|
+
const name = sanitizeName(basename(root)) || "service";
|
|
8
|
+
const pkg = await readJson(join(root, "package.json"));
|
|
9
|
+
if (pkg) {
|
|
10
|
+
const manager = await detectPackageManager(root);
|
|
11
|
+
const scripts = (pkg.scripts ?? {});
|
|
12
|
+
const script = ["dev", "start", "serve"].find((key) => typeof scripts[key] === "string");
|
|
13
|
+
const command = script
|
|
14
|
+
? manager === "npm"
|
|
15
|
+
? `npm run ${script}`
|
|
16
|
+
: `${manager} ${script}`
|
|
17
|
+
: undefined;
|
|
18
|
+
const port = script ? portFrom(scripts[script]) : undefined;
|
|
19
|
+
return {
|
|
20
|
+
kind: "local",
|
|
21
|
+
name,
|
|
22
|
+
command,
|
|
23
|
+
port: port ?? (await portFromEnv(root)),
|
|
24
|
+
description: typeof pkg.description === "string" ? pkg.description : undefined,
|
|
25
|
+
detected: script
|
|
26
|
+
? `package.json — ${manager} "${script}" script`
|
|
27
|
+
: `package.json — no dev/start script found, pick a command`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const compose = await firstExisting(root, [
|
|
31
|
+
"docker-compose.yml",
|
|
32
|
+
"docker-compose.yaml",
|
|
33
|
+
"compose.yml",
|
|
34
|
+
"compose.yaml",
|
|
35
|
+
]);
|
|
36
|
+
if (compose) {
|
|
37
|
+
const service = await firstComposeService(join(root, compose));
|
|
38
|
+
return {
|
|
39
|
+
kind: "docker-compose",
|
|
40
|
+
name,
|
|
41
|
+
composeFile: compose,
|
|
42
|
+
composeService: service,
|
|
43
|
+
detected: `${compose}${service ? ` — first service "${service}"` : ""}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const langGuess = await detectLanguageCommand(root);
|
|
47
|
+
if (langGuess) {
|
|
48
|
+
return { kind: "local", name, command: langGuess.command, detected: langGuess.detected };
|
|
49
|
+
}
|
|
50
|
+
return { kind: "local", name, detected: "No known project files — enter a command manually" };
|
|
51
|
+
}
|
|
52
|
+
async function detectPackageManager(root) {
|
|
53
|
+
if (await exists(join(root, "pnpm-lock.yaml")))
|
|
54
|
+
return "pnpm";
|
|
55
|
+
if (await exists(join(root, "yarn.lock")))
|
|
56
|
+
return "yarn";
|
|
57
|
+
if (await exists(join(root, "bun.lockb")))
|
|
58
|
+
return "bun";
|
|
59
|
+
return "npm";
|
|
60
|
+
}
|
|
61
|
+
async function detectLanguageCommand(root) {
|
|
62
|
+
if (await exists(join(root, "go.mod"))) {
|
|
63
|
+
return { command: "go run .", detected: "go.mod — Go module" };
|
|
64
|
+
}
|
|
65
|
+
if (await exists(join(root, "Cargo.toml"))) {
|
|
66
|
+
return { command: "cargo run", detected: "Cargo.toml — Rust crate" };
|
|
67
|
+
}
|
|
68
|
+
if (await exists(join(root, "manage.py"))) {
|
|
69
|
+
return { command: "python manage.py runserver", detected: "manage.py — Django project" };
|
|
70
|
+
}
|
|
71
|
+
if ((await exists(join(root, "pyproject.toml"))) || (await exists(join(root, "requirements.txt")))) {
|
|
72
|
+
return { command: "python main.py", detected: "Python project — adjust the entry point" };
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
function portFrom(script) {
|
|
77
|
+
if (!script)
|
|
78
|
+
return undefined;
|
|
79
|
+
const match = script.match(PORT_IN_SCRIPT);
|
|
80
|
+
const port = match ? Number(match[1]) : NaN;
|
|
81
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : undefined;
|
|
82
|
+
}
|
|
83
|
+
async function portFromEnv(root) {
|
|
84
|
+
const text = await readText(join(root, ".env"));
|
|
85
|
+
if (!text)
|
|
86
|
+
return undefined;
|
|
87
|
+
const line = text.split("\n").find((entry) => /^\s*PORT\s*=/.test(entry));
|
|
88
|
+
return portFrom(line);
|
|
89
|
+
}
|
|
90
|
+
/** Pull the first key under a compose file's top-level `services:` block. */
|
|
91
|
+
async function firstComposeService(path) {
|
|
92
|
+
const text = await readText(path);
|
|
93
|
+
if (!text)
|
|
94
|
+
return undefined;
|
|
95
|
+
const lines = text.split("\n");
|
|
96
|
+
const start = lines.findIndex((line) => /^services:\s*$/.test(line));
|
|
97
|
+
if (start === -1)
|
|
98
|
+
return undefined;
|
|
99
|
+
for (const line of lines.slice(start + 1)) {
|
|
100
|
+
if (/^\S/.test(line))
|
|
101
|
+
break; // dedented back to another top-level key
|
|
102
|
+
const match = line.match(/^\s{1,4}([A-Za-z0-9._-]+):\s*$/);
|
|
103
|
+
if (match)
|
|
104
|
+
return match[1];
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
async function firstExisting(root, names) {
|
|
109
|
+
for (const name of names) {
|
|
110
|
+
if (await exists(join(root, name)))
|
|
111
|
+
return name;
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
function sanitizeName(raw) {
|
|
116
|
+
return raw.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
117
|
+
}
|
|
118
|
+
async function exists(path) {
|
|
119
|
+
return access(path).then(() => true, () => false);
|
|
120
|
+
}
|
|
121
|
+
async function readText(path) {
|
|
122
|
+
try {
|
|
123
|
+
return await readFile(path, "utf8");
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function readJson(path) {
|
|
130
|
+
const text = await readText(path);
|
|
131
|
+
if (!text)
|
|
132
|
+
return undefined;
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(text);
|
|
135
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=service-detect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-detect.js","sourceRoot":"","sources":["../../src/core/service-detect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { SshServerDefinition } from "./types.js";
|
|
2
|
+
export interface SshServerSummary extends SshServerDefinition {
|
|
3
|
+
discovered: boolean;
|
|
4
|
+
saved: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface SshServerProbe {
|
|
7
|
+
host: string;
|
|
8
|
+
reachable: boolean;
|
|
9
|
+
latencyMs: number;
|
|
10
|
+
hostname?: string;
|
|
11
|
+
platform?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface RemoteProcessMetric {
|
|
15
|
+
pid: number;
|
|
16
|
+
ppid: number;
|
|
17
|
+
user: string;
|
|
18
|
+
cpuPercent: number;
|
|
19
|
+
rssMb: number;
|
|
20
|
+
command: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RemoteHostMetricSample {
|
|
23
|
+
t: number;
|
|
24
|
+
cpuPercent: number;
|
|
25
|
+
memoryUsedBytes: number;
|
|
26
|
+
memoryTotalBytes: number;
|
|
27
|
+
memoryUsedPercent: number;
|
|
28
|
+
loadAverage: [number, number, number];
|
|
29
|
+
uptimeSeconds: number;
|
|
30
|
+
logicalCpuCount: number;
|
|
31
|
+
disk: {
|
|
32
|
+
path: string;
|
|
33
|
+
totalBytes: number;
|
|
34
|
+
usedBytes: number;
|
|
35
|
+
availableBytes: number;
|
|
36
|
+
usedPercent: number;
|
|
37
|
+
} | null;
|
|
38
|
+
}
|
|
39
|
+
export interface RemoteHostMetrics {
|
|
40
|
+
host: string;
|
|
41
|
+
hostname: string;
|
|
42
|
+
platform: string;
|
|
43
|
+
current: RemoteHostMetricSample;
|
|
44
|
+
processes: RemoteProcessMetric[];
|
|
45
|
+
}
|
|
46
|
+
/** Explicit, concrete aliases from ~/.ssh/config. Wildcard patterns are omitted. */
|
|
47
|
+
export declare function parseSshConfigHosts(contents: string): string[];
|
|
48
|
+
export declare function discoverSshHosts(configPath?: string): Promise<string[]>;
|
|
49
|
+
export declare function mergeSshServers(saved: SshServerDefinition[], discoveredHosts: string[]): SshServerSummary[];
|
|
50
|
+
export declare function probeSshServer(host: string): Promise<SshServerProbe>;
|
|
51
|
+
export declare function readRemoteHostMetrics(host: string): Promise<RemoteHostMetrics>;
|
|
52
|
+
export declare function parseRemoteMetrics(host: string, output: string, sampledAt: number): RemoteHostMetrics;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const SSH_TIMEOUT_MS = 7_000;
|
|
8
|
+
const SSH_OPTIONS = [
|
|
9
|
+
"-o",
|
|
10
|
+
"BatchMode=yes",
|
|
11
|
+
"-o",
|
|
12
|
+
"ConnectTimeout=5",
|
|
13
|
+
];
|
|
14
|
+
/** Explicit, concrete aliases from ~/.ssh/config. Wildcard patterns are omitted. */
|
|
15
|
+
export function parseSshConfigHosts(contents) {
|
|
16
|
+
const hosts = new Set();
|
|
17
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
18
|
+
const line = rawLine.trim();
|
|
19
|
+
const match = /^Host\s+(.+)$/i.exec(line);
|
|
20
|
+
if (!match)
|
|
21
|
+
continue;
|
|
22
|
+
for (const candidate of (match[1] ?? "").split(/\s+/)) {
|
|
23
|
+
if (!candidate || /[*?!]/.test(candidate) || candidate.startsWith("-"))
|
|
24
|
+
continue;
|
|
25
|
+
hosts.add(candidate);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return [...hosts].sort((a, b) => a.localeCompare(b));
|
|
29
|
+
}
|
|
30
|
+
export async function discoverSshHosts(configPath = join(homedir(), ".ssh", "config")) {
|
|
31
|
+
const contents = await readFile(configPath, "utf8").catch(() => "");
|
|
32
|
+
return parseSshConfigHosts(contents);
|
|
33
|
+
}
|
|
34
|
+
export function mergeSshServers(saved, discoveredHosts) {
|
|
35
|
+
const savedByHost = new Map(saved.map((server) => [server.host, server]));
|
|
36
|
+
const hosts = new Set([...discoveredHosts, ...savedByHost.keys()]);
|
|
37
|
+
return [...hosts]
|
|
38
|
+
.sort((a, b) => a.localeCompare(b))
|
|
39
|
+
.map((host) => ({
|
|
40
|
+
host,
|
|
41
|
+
...savedByHost.get(host),
|
|
42
|
+
discovered: discoveredHosts.includes(host),
|
|
43
|
+
saved: savedByHost.has(host),
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
export async function probeSshServer(host) {
|
|
47
|
+
assertSafeSshHost(host);
|
|
48
|
+
const startedAt = Date.now();
|
|
49
|
+
try {
|
|
50
|
+
const { stdout } = await execFileAsync("ssh", [...SSH_OPTIONS, host, "printf 'NMI\\t'; hostname; uname -s"], { timeout: SSH_TIMEOUT_MS, maxBuffer: 64 * 1024 });
|
|
51
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
52
|
+
const first = lines[0]?.split("\t") ?? [];
|
|
53
|
+
if (first[0] !== "NMI")
|
|
54
|
+
throw new Error("Remote probe returned an unexpected response.");
|
|
55
|
+
return {
|
|
56
|
+
host,
|
|
57
|
+
reachable: true,
|
|
58
|
+
latencyMs: Date.now() - startedAt,
|
|
59
|
+
hostname: first[1]?.trim() || undefined,
|
|
60
|
+
platform: lines[1]?.trim() || undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
host,
|
|
66
|
+
reachable: false,
|
|
67
|
+
latencyMs: Date.now() - startedAt,
|
|
68
|
+
error: conciseSshError(error),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function readRemoteHostMetrics(host) {
|
|
73
|
+
assertSafeSshHost(host);
|
|
74
|
+
const { stdout } = await execFileAsync("ssh", [...SSH_OPTIONS, host, REMOTE_METRICS_COMMAND], { timeout: SSH_TIMEOUT_MS, maxBuffer: 2 * 1024 * 1024 });
|
|
75
|
+
return parseRemoteMetrics(host, stdout, Date.now());
|
|
76
|
+
}
|
|
77
|
+
export function parseRemoteMetrics(host, output, sampledAt) {
|
|
78
|
+
const lines = output.split(/\r?\n/);
|
|
79
|
+
const values = new Map();
|
|
80
|
+
const processes = [];
|
|
81
|
+
let readingProcesses = false;
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (line === "NMI_PROCESSES") {
|
|
84
|
+
readingProcesses = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (readingProcesses) {
|
|
88
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
89
|
+
if (!match)
|
|
90
|
+
continue;
|
|
91
|
+
processes.push({
|
|
92
|
+
pid: Number(match[1]),
|
|
93
|
+
ppid: Number(match[2]),
|
|
94
|
+
user: match[3] ?? "",
|
|
95
|
+
cpuPercent: Number(match[4]),
|
|
96
|
+
rssMb: Number(match[5]) / 1024,
|
|
97
|
+
command: match[6] ?? "",
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const [key, ...rest] = line.split("\t");
|
|
102
|
+
if (key?.startsWith("NMI_"))
|
|
103
|
+
values.set(key, rest);
|
|
104
|
+
}
|
|
105
|
+
const meta = required(values, "NMI_META", 2);
|
|
106
|
+
const cpu = numberAt(values, "NMI_CPU", 0);
|
|
107
|
+
const memory = required(values, "NMI_MEMORY", 2).map(Number);
|
|
108
|
+
const load = required(values, "NMI_LOAD", 3).map(Number);
|
|
109
|
+
const uptimeSeconds = numberAt(values, "NMI_UPTIME", 0);
|
|
110
|
+
const logicalCpuCount = numberAt(values, "NMI_CPUS", 0);
|
|
111
|
+
const diskValues = values.get("NMI_DISK")?.map(Number);
|
|
112
|
+
const memoryTotalBytes = memory[0] * 1024;
|
|
113
|
+
const memoryAvailableBytes = memory[1] * 1024;
|
|
114
|
+
const memoryUsedBytes = Math.max(0, memoryTotalBytes - memoryAvailableBytes);
|
|
115
|
+
const disk = diskValues && diskValues.length >= 3
|
|
116
|
+
? {
|
|
117
|
+
path: "/",
|
|
118
|
+
totalBytes: diskValues[0] * 1024,
|
|
119
|
+
usedBytes: diskValues[1] * 1024,
|
|
120
|
+
availableBytes: diskValues[2] * 1024,
|
|
121
|
+
usedPercent: percent(diskValues[1], diskValues[0]),
|
|
122
|
+
}
|
|
123
|
+
: null;
|
|
124
|
+
return {
|
|
125
|
+
host,
|
|
126
|
+
hostname: meta[0] ?? host,
|
|
127
|
+
platform: meta[1] ?? "Linux",
|
|
128
|
+
current: {
|
|
129
|
+
t: sampledAt,
|
|
130
|
+
cpuPercent: cpu,
|
|
131
|
+
memoryUsedBytes,
|
|
132
|
+
memoryTotalBytes,
|
|
133
|
+
memoryUsedPercent: percent(memoryUsedBytes, memoryTotalBytes),
|
|
134
|
+
loadAverage: load,
|
|
135
|
+
uptimeSeconds,
|
|
136
|
+
logicalCpuCount,
|
|
137
|
+
disk,
|
|
138
|
+
},
|
|
139
|
+
processes,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function assertSafeSshHost(host) {
|
|
143
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:@-]*$/.test(host)) {
|
|
144
|
+
throw new Error("SSH host must be a host name or ~/.ssh/config alias.");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function conciseSshError(error) {
|
|
148
|
+
if (!(error instanceof Error))
|
|
149
|
+
return String(error);
|
|
150
|
+
const stderr = "stderr" in error ? String(error.stderr).trim() : "";
|
|
151
|
+
return (stderr || error.message).split(/\r?\n/).at(-1) ?? error.message;
|
|
152
|
+
}
|
|
153
|
+
function required(values, key, length) {
|
|
154
|
+
const value = values.get(key);
|
|
155
|
+
if (!value || value.length < length) {
|
|
156
|
+
throw new Error(`Remote metrics response is missing ${key}.`);
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function numberAt(values, key, index) {
|
|
161
|
+
const value = Number(required(values, key, index + 1)[index]);
|
|
162
|
+
if (!Number.isFinite(value))
|
|
163
|
+
throw new Error(`Remote metrics response has invalid ${key}.`);
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
function percent(value, total) {
|
|
167
|
+
if (total <= 0)
|
|
168
|
+
return 0;
|
|
169
|
+
return Math.round(Math.min(100, Math.max(0, (value / total) * 100)) * 10) / 10;
|
|
170
|
+
}
|
|
171
|
+
// Linux-first, read-only probe. Output is deliberately tab/line based so the
|
|
172
|
+
// remote host needs only POSIX sh, awk, ps, and procfs — not Node or Python.
|
|
173
|
+
const REMOTE_METRICS_COMMAND = String.raw `LC_ALL=C sh -c '
|
|
174
|
+
read_cpu() { awk '\''/^cpu / { idle=$5+$6; total=0; for(i=2;i<=NF;i++) total+=$i; print idle, total; exit }'\'' /proc/stat; }
|
|
175
|
+
set -- $(read_cpu); idle1=$1; total1=$2
|
|
176
|
+
sleep 0.2
|
|
177
|
+
set -- $(read_cpu); idle2=$1; total2=$2
|
|
178
|
+
cpu=$(awk -v i1="$idle1" -v t1="$total1" -v i2="$idle2" -v t2="$total2" '\''BEGIN { d=t2-t1; if (d<=0) print "0.0"; else printf "%.1f", (1-(i2-i1)/d)*100 }'\'')
|
|
179
|
+
mem_total=$(awk '\''/^MemTotal:/ {print $2}'\'' /proc/meminfo)
|
|
180
|
+
mem_available=$(awk '\''/^MemAvailable:/ {print $2}'\'' /proc/meminfo)
|
|
181
|
+
set -- $(cat /proc/loadavg); load1=$1; load5=$2; load15=$3
|
|
182
|
+
set -- $(cat /proc/uptime); uptime=$1
|
|
183
|
+
set -- $(df -Pk / | tail -n 1); disk_total=$2; disk_used=$3; disk_available=$4
|
|
184
|
+
printf "NMI_META\t%s\t%s\n" "$(hostname)" "$(uname -s)"
|
|
185
|
+
printf "NMI_CPU\t%s\n" "$cpu"
|
|
186
|
+
printf "NMI_MEMORY\t%s\t%s\n" "$mem_total" "$mem_available"
|
|
187
|
+
printf "NMI_LOAD\t%s\t%s\t%s\n" "$load1" "$load5" "$load15"
|
|
188
|
+
printf "NMI_UPTIME\t%s\n" "$uptime"
|
|
189
|
+
printf "NMI_CPUS\t%s\n" "$(getconf _NPROCESSORS_ONLN 2>/dev/null || grep -c ^processor /proc/cpuinfo)"
|
|
190
|
+
printf "NMI_DISK\t%s\t%s\t%s\n" "$disk_total" "$disk_used" "$disk_available"
|
|
191
|
+
printf "NMI_PROCESSES\n"
|
|
192
|
+
ps -eo pid=,ppid=,user=,pcpu=,rss=,args= --sort=-pcpu | head -n 101
|
|
193
|
+
'`;
|
|
194
|
+
//# sourceMappingURL=ssh-servers.js.map
|