@nema-project/nema 0.0.0-stage → 0.1.4
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/LICENSE +21 -0
- package/dist/chunks/chunk-HEIQINX3.js +825 -0
- package/dist/chunks/chunk-WT3XUFWU.js +86 -0
- package/dist/claude/adapter.d.ts +19 -0
- package/dist/claude/adapter.js +258 -0
- package/dist/claude/events.d.ts +29 -0
- package/dist/claude/events.js +75 -0
- package/dist/claude/launch.d.ts +22 -0
- package/dist/claude/launch.js +80 -0
- package/dist/claude/token.d.ts +9 -0
- package/dist/claude/token.js +87 -0
- package/dist/codex/adapter.d.ts +17 -0
- package/dist/codex/adapter.js +151 -0
- package/dist/codex/events.d.ts +16 -0
- package/dist/codex/events.js +47 -0
- package/dist/codex/isolation.d.ts +33 -0
- package/dist/codex/isolation.js +115 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +75 -0
- package/dist/configCommands.d.ts +13 -0
- package/dist/configCommands.js +69 -0
- package/dist/daemon/cleanup.d.ts +5 -0
- package/dist/daemon/cleanup.js +49 -0
- package/dist/daemon/connection.d.ts +22 -0
- package/dist/daemon/connection.js +166 -0
- package/dist/daemon/rpc.d.ts +14 -0
- package/dist/daemon/rpc.js +20 -0
- package/dist/daemon/runner.d.ts +34 -0
- package/dist/daemon/runner.js +94 -0
- package/dist/daemon/setup.d.ts +39 -0
- package/dist/daemon/setup.js +95 -0
- package/dist/direct/adapter.d.ts +30 -0
- package/dist/direct/adapter.js +190 -0
- package/dist/direct/authCommands.d.ts +54 -0
- package/dist/direct/authCommands.js +268 -0
- package/dist/direct/authStore.d.ts +120 -0
- package/dist/direct/authStore.js +351 -0
- package/dist/direct/byok.d.ts +27 -0
- package/dist/direct/byok.js +67 -0
- package/dist/direct/codexLogin.d.ts +34 -0
- package/dist/direct/codexLogin.js +93 -0
- package/dist/direct/jwt.d.ts +4 -0
- package/dist/direct/jwt.js +22 -0
- package/dist/direct/lock.d.ts +16 -0
- package/dist/direct/lock.js +134 -0
- package/dist/direct/loop.d.ts +33 -0
- package/dist/direct/loop.js +110 -0
- package/dist/direct/siwc.d.ts +63 -0
- package/dist/direct/siwc.js +403 -0
- package/dist/direct/tools.d.ts +17 -0
- package/dist/direct/tools.js +104 -0
- package/dist/direct/transport.d.ts +177 -0
- package/dist/direct/transport.js +437 -0
- package/dist/doctor.d.ts +28 -0
- package/dist/doctor.js +178 -0
- package/dist/extensions/handlers.d.ts +2 -0
- package/dist/extensions/handlers.js +10 -0
- package/dist/extensions/store.d.ts +11 -0
- package/dist/extensions/store.js +132 -0
- package/dist/github/client.d.ts +47 -0
- package/dist/github/client.js +383 -0
- package/dist/github/gh.d.ts +56 -0
- package/dist/github/gh.js +385 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +144 -0
- package/dist/localSettings.d.ts +8 -0
- package/dist/localSettings.js +26 -0
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +83 -0
- package/dist/ownedProcess.d.ts +39 -0
- package/dist/ownedProcess.js +229 -0
- package/dist/platform.d.ts +10 -0
- package/dist/platform.js +60 -0
- package/dist/report/commands.d.ts +41 -0
- package/dist/report/commands.js +159 -0
- package/dist/report/desktop.d.ts +3 -0
- package/dist/report/desktop.js +123 -0
- package/dist/report/git.d.ts +5 -0
- package/dist/report/git.js +42 -0
- package/dist/report/install.d.ts +22 -0
- package/dist/report/install.js +183 -0
- package/dist/report/send.d.ts +35 -0
- package/dist/report/send.js +208 -0
- package/dist/report/token.d.ts +5 -0
- package/dist/report/token.js +86 -0
- package/dist/repos/git.d.ts +48 -0
- package/dist/repos/git.js +370 -0
- package/dist/repos/graphify.d.ts +32 -0
- package/dist/repos/graphify.js +292 -0
- package/dist/repos/handlers.d.ts +20 -0
- package/dist/repos/handlers.js +255 -0
- package/dist/repos/indexState.d.ts +10 -0
- package/dist/repos/indexState.js +45 -0
- package/dist/repos/pickDirectory.d.ts +11 -0
- package/dist/repos/pickDirectory.js +140 -0
- package/dist/repos/reportCheck.d.ts +13 -0
- package/dist/repos/reportCheck.js +268 -0
- package/dist/repos/watcher.d.ts +64 -0
- package/dist/repos/watcher.js +202 -0
- package/dist/room/client.d.ts +26 -0
- package/dist/room/client.js +76 -0
- package/dist/secrets/file.d.ts +12 -0
- package/dist/secrets/file.js +89 -0
- package/dist/secrets/github.d.ts +10 -0
- package/dist/secrets/github.js +29 -0
- package/dist/secrets/permissions.d.ts +7 -0
- package/dist/secrets/permissions.js +86 -0
- package/dist/systemGit.d.ts +14 -0
- package/dist/systemGit.js +75 -0
- package/dist/wp/commands.d.ts +15 -0
- package/dist/wp/commands.js +103 -0
- package/package.json +26 -4
- package/src/report/SKILL.md +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// secrets/file.js
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { closeSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { openPrivateFile, privateModeMatches, protectPrivatePath } from "./permissions.js";
|
|
7
|
+
var SecretFileError = class extends Error {
|
|
8
|
+
kind;
|
|
9
|
+
constructor(kind, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.kind = kind;
|
|
12
|
+
this.name = "SecretFileError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var INVALID_VALUE = "\u5FC5\u987B\u662F\u4E00\u884C\u975E\u7A7A\u6587\u672C";
|
|
16
|
+
var isValidLine = (value) => value !== "" && !/[\u0000-\u001f\u007f]/.test(value);
|
|
17
|
+
var symlinkError = (file) => new SecretFileError("symlink", `${path.basename(file)} \u6587\u4EF6\u662F\u7B26\u53F7\u94FE\u63A5\uFF0C\u5DF2\u62D2\u7EDD\u3002`);
|
|
18
|
+
function saveSecretLine(file, value, dirMode = 448) {
|
|
19
|
+
if (!isValidLine(value))
|
|
20
|
+
throw new SecretFileError("invalid", INVALID_VALUE);
|
|
21
|
+
try {
|
|
22
|
+
if (lstatSync(file).isSymbolicLink())
|
|
23
|
+
throw symlinkError(file);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (e.code !== "ENOENT")
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
const dir = path.dirname(file);
|
|
29
|
+
mkdirSync(dir, { recursive: true, mode: dirMode });
|
|
30
|
+
protectPrivatePath(dir, dirMode);
|
|
31
|
+
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
32
|
+
const fd = openSync(tmp, "wx", 384);
|
|
33
|
+
try {
|
|
34
|
+
try {
|
|
35
|
+
writeSync(fd, value);
|
|
36
|
+
fsyncSync(fd);
|
|
37
|
+
} finally {
|
|
38
|
+
closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
protectPrivatePath(tmp, 384);
|
|
41
|
+
renameSync(tmp, file);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
try {
|
|
44
|
+
unlinkSync(tmp);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function readSecretLine(file, missingMessage, invalidMessage) {
|
|
51
|
+
let fd;
|
|
52
|
+
try {
|
|
53
|
+
fd = openPrivateFile(file);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
if (e.code === "ENOENT")
|
|
56
|
+
throw new Error(missingMessage);
|
|
57
|
+
if (e.code === "ELOOP")
|
|
58
|
+
throw symlinkError(file);
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const st = fstatSync(fd);
|
|
63
|
+
if (!st.isFile() || !privateModeMatches(st)) {
|
|
64
|
+
throw new SecretFileError("mode", `${path.basename(file)} \u6587\u4EF6\u6743\u9650\u5FC5\u987B\u662F 0600`);
|
|
65
|
+
}
|
|
66
|
+
const value = readFileSync(fd, "utf8").replace(/\r?\n$/, "");
|
|
67
|
+
if (!isValidLine(value))
|
|
68
|
+
throw new Error(invalidMessage);
|
|
69
|
+
return value;
|
|
70
|
+
} finally {
|
|
71
|
+
closeSync(fd);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function readStdinLine(stdin) {
|
|
75
|
+
const lines = createInterface({ input: stdin });
|
|
76
|
+
let value = "";
|
|
77
|
+
for await (const line of lines) {
|
|
78
|
+
value = line.trim();
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
lines.close();
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
SecretFileError,
|
|
86
|
+
readSecretLine,
|
|
87
|
+
readStdinLine,
|
|
88
|
+
saveSecretLine
|
|
89
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Out } from "../direct/authCommands.js";
|
|
2
|
+
export declare const githubTokenPath: (nemaHome: string) => string;
|
|
3
|
+
export declare function saveGithubToken(nemaHome: string, token: string): void;
|
|
4
|
+
export declare function readGithubToken(nemaHome: string): string;
|
|
5
|
+
/** `nema auth github`: reads one line from stdin. Never prints the token. */
|
|
6
|
+
export declare function authGithub(o: {
|
|
7
|
+
nemaHome: string;
|
|
8
|
+
out: Out;
|
|
9
|
+
stdin: NodeJS.ReadableStream;
|
|
10
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// secrets/github.js
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readSecretLine, readStdinLine, saveSecretLine } from "./file.js";
|
|
4
|
+
var MISSING_TOKEN = "\u672A\u4FDD\u5B58 GitHub \u4EE4\u724C\uFF1A\u8BF7\u8FD0\u884C nema auth github";
|
|
5
|
+
var INVALID_STORED_TOKEN = "GitHub \u4EE4\u724C\u6587\u4EF6\u5185\u5BB9\u65E0\u6548\u3002\u8BF7\u91CD\u65B0\u628A\u4EE4\u724C\u4ECE\u6807\u51C6\u8F93\u5165\u4EA4\u7ED9 nema auth github\u3002";
|
|
6
|
+
var githubTokenPath = (nemaHome) => path.join(nemaHome, "github-token");
|
|
7
|
+
function saveGithubToken(nemaHome, token) {
|
|
8
|
+
saveSecretLine(githubTokenPath(nemaHome), token);
|
|
9
|
+
}
|
|
10
|
+
function readGithubToken(nemaHome) {
|
|
11
|
+
return readSecretLine(githubTokenPath(nemaHome), MISSING_TOKEN, INVALID_STORED_TOKEN);
|
|
12
|
+
}
|
|
13
|
+
async function authGithub(o) {
|
|
14
|
+
const token = await readStdinLine(o.stdin);
|
|
15
|
+
try {
|
|
16
|
+
saveGithubToken(o.nemaHome, token);
|
|
17
|
+
o.out.log("\u5DF2\u4FDD\u5B58 GitHub \u4EE4\u724C");
|
|
18
|
+
return 0;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
o.out.error(e instanceof Error ? e.message : String(e));
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
authGithub,
|
|
26
|
+
githubTokenPath,
|
|
27
|
+
readGithubToken,
|
|
28
|
+
saveGithubToken
|
|
29
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const WINDOWS_ACL_SCRIPT: string;
|
|
2
|
+
export declare function protectPrivatePath(file: string, mode?: number): void;
|
|
3
|
+
export declare function privateModeMatches(st: {
|
|
4
|
+
mode: number;
|
|
5
|
+
}, platform?: NodeJS.Platform): boolean;
|
|
6
|
+
/** Refuse links on all platforms, and verify the Windows DACL and opened file identity. */
|
|
7
|
+
export declare function openPrivateFile(file: string): number;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// secrets/permissions.js
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { chmodSync, closeSync, constants, fstatSync, lstatSync, openSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
var WINDOWS_ACL_SCRIPT = String.raw`
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
$file = $env:NEMA_PRIVATE_PATH
|
|
8
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
9
|
+
if ([System.IO.Directory]::Exists($file)) { $item = [System.IO.DirectoryInfo]::new($file); $isDirectory = $true }
|
|
10
|
+
elseif ([System.IO.File]::Exists($file)) { $item = [System.IO.FileInfo]::new($file); $isDirectory = $false }
|
|
11
|
+
else { throw 'Path not found' }
|
|
12
|
+
if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw 'Reparse point refused' }
|
|
13
|
+
if ($env:NEMA_PRIVATE_ACTION -eq 'protect') {
|
|
14
|
+
if ($isDirectory) { $acl = [System.Security.AccessControl.DirectorySecurity]::new() }
|
|
15
|
+
else { $acl = [System.Security.AccessControl.FileSecurity]::new() }
|
|
16
|
+
$acl.SetOwner($sid)
|
|
17
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
18
|
+
foreach ($principal in @($sid.Value, 'S-1-5-18', 'S-1-5-32-544')) {
|
|
19
|
+
$identity = [System.Security.Principal.SecurityIdentifier]::new($principal)
|
|
20
|
+
if ($isDirectory) {
|
|
21
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($identity, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
|
|
22
|
+
} else {
|
|
23
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($identity, 'FullControl', 'Allow')
|
|
24
|
+
}
|
|
25
|
+
$acl.AddAccessRule($rule)
|
|
26
|
+
}
|
|
27
|
+
$item.SetAccessControl($acl)
|
|
28
|
+
}
|
|
29
|
+
$acl = $item.GetAccessControl()
|
|
30
|
+
if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'Unexpected owner' }
|
|
31
|
+
$allowed = @($sid.Value, 'S-1-5-18', 'S-1-5-32-544')
|
|
32
|
+
$hasOwner = $false
|
|
33
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
|
|
34
|
+
if ($rule.AccessControlType -eq 'Allow') {
|
|
35
|
+
if ($allowed -notcontains $rule.IdentityReference.Value) { throw 'Non-private ACL' }
|
|
36
|
+
if ($rule.IdentityReference.Value -eq $sid.Value -and ($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::ReadData)) { $hasOwner = $true }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (-not $hasOwner) { throw 'Owner cannot read' }
|
|
40
|
+
`;
|
|
41
|
+
function windowsAcl(file, action) {
|
|
42
|
+
const exe = path.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
43
|
+
const result = spawnSync(exe, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", WINDOWS_ACL_SCRIPT], {
|
|
44
|
+
env: { ...process.env, NEMA_PRIVATE_PATH: path.resolve(file), NEMA_PRIVATE_ACTION: action },
|
|
45
|
+
// A cold PowerShell on a busy Windows runner can take longer than a local call.
|
|
46
|
+
stdio: "ignore",
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
timeout: 45e3
|
|
49
|
+
});
|
|
50
|
+
if (result.error || result.status !== 0) {
|
|
51
|
+
const reason = result.error?.code ?? (result.status === null ? "unknown" : `powershell exit ${result.status}`);
|
|
52
|
+
throw new Error(`${path.basename(file)}: \u65E0\u6CD5\u9A8C\u8BC1\u79C1\u6709 Windows ACL / cannot verify private Windows ACL (${reason})`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function protectPrivatePath(file, mode = 384) {
|
|
56
|
+
if (process.platform === "win32")
|
|
57
|
+
windowsAcl(file, "protect");
|
|
58
|
+
else
|
|
59
|
+
chmodSync(file, mode);
|
|
60
|
+
}
|
|
61
|
+
function privateModeMatches(st, platform = process.platform) {
|
|
62
|
+
return platform === "win32" || (st.mode & 511) === 384;
|
|
63
|
+
}
|
|
64
|
+
function openPrivateFile(file) {
|
|
65
|
+
const before = lstatSync(file);
|
|
66
|
+
if (before.isSymbolicLink())
|
|
67
|
+
throw Object.assign(new Error("Symbolic link refused"), { code: "ELOOP" });
|
|
68
|
+
const fd = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
69
|
+
try {
|
|
70
|
+
const opened = fstatSync(fd);
|
|
71
|
+
if (!opened.isFile() || before.dev !== opened.dev || before.ino !== opened.ino)
|
|
72
|
+
throw new Error("Private file changed while opening");
|
|
73
|
+
if (process.platform === "win32")
|
|
74
|
+
windowsAcl(file, "check");
|
|
75
|
+
return fd;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
closeSync(fd);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
WINDOWS_ACL_SCRIPT,
|
|
83
|
+
openPrivateFile,
|
|
84
|
+
privateModeMatches,
|
|
85
|
+
protectPrivatePath
|
|
86
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** `rev-parse --path-format=absolute` (worktree mapping) needs Git 2.31. */
|
|
2
|
+
export declare const MIN_GIT_VERSION: readonly [2, 31, 0];
|
|
3
|
+
export declare function parseGitVersion(output: string): [number, number, number] | null;
|
|
4
|
+
/** PATH a Mac app gets when Finder opens it. Anything else is left alone, including a test that cleared PATH. */
|
|
5
|
+
export declare const FINDER_GIT_PATH: string[];
|
|
6
|
+
/** Install locations Finder does not put on PATH. Homebrew (Apple silicon, then Intel) and MacPorts. */
|
|
7
|
+
export declare const EXTRA_GIT_DIRECTORIES: string[];
|
|
8
|
+
export declare function gitPathForFinderLaunch(currentPath: string | undefined, presentDirectories: readonly string[], platform?: NodeJS.Platform): string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Nema runs the git the user already has, so fetch uses their own credential helpers and SSH setup.
|
|
11
|
+
* A Mac app opened from Finder only has /usr/bin on PATH; Homebrew and MacPorts git are added in that case.
|
|
12
|
+
* Resolves with the `git --version` line; rejects with code NEMA_GIT_UNAVAILABLE and install guidance.
|
|
13
|
+
*/
|
|
14
|
+
export declare function requireSystemGit(run?: () => Promise<string>): Promise<string>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// systemGit.js
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { accessSync, constants } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
var execFileAsync = promisify(execFile);
|
|
7
|
+
var MIN_GIT_VERSION = [2, 31, 0];
|
|
8
|
+
var MIN_TEXT = MIN_GIT_VERSION.join(".");
|
|
9
|
+
var INSTALL_URL = "https://git-scm.com/downloads";
|
|
10
|
+
function parseGitVersion(output) {
|
|
11
|
+
const match = /git version (\d+)\.(\d+)(?:\.(\d+))?/.exec(output);
|
|
12
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)] : null;
|
|
13
|
+
}
|
|
14
|
+
function meetsMinimum(version) {
|
|
15
|
+
for (let i = 0; i < 3; i++)
|
|
16
|
+
if (version[i] !== MIN_GIT_VERSION[i])
|
|
17
|
+
return version[i] > MIN_GIT_VERSION[i];
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
function unavailable(reason, cause) {
|
|
21
|
+
return Object.assign(new Error(`Nema \u9700\u8981 Git ${MIN_TEXT} \u6216\u66F4\u9AD8\u7248\u672C\uFF1A${reason.zh}\u3002\u8BF7\u4ECE ${INSTALL_URL} \u5B89\u88C5\u540E\u91CD\u65B0\u6253\u5F00 Nema\u3002 / Nema needs Git ${MIN_TEXT} or later: ${reason.en}. Install it from ${INSTALL_URL} and reopen Nema.`, { cause }), { code: "NEMA_GIT_UNAVAILABLE" });
|
|
22
|
+
}
|
|
23
|
+
var FINDER_GIT_PATH = ["/usr/bin", "/bin", "/usr/sbin", "/sbin"];
|
|
24
|
+
var EXTRA_GIT_DIRECTORIES = ["/opt/homebrew/bin", "/usr/local/bin", "/opt/local/bin"];
|
|
25
|
+
function gitPathForFinderLaunch(currentPath, presentDirectories, platform = process.platform) {
|
|
26
|
+
if (platform !== "darwin")
|
|
27
|
+
return currentPath;
|
|
28
|
+
const parts = (currentPath ?? "").split(":").filter(Boolean);
|
|
29
|
+
if (parts.length === 0 || parts.some((part) => !FINDER_GIT_PATH.includes(part)))
|
|
30
|
+
return currentPath;
|
|
31
|
+
const prefix = presentDirectories.filter((dir) => EXTRA_GIT_DIRECTORIES.includes(dir) && !parts.includes(dir));
|
|
32
|
+
return prefix.length ? [...prefix, ...parts].join(":") : currentPath;
|
|
33
|
+
}
|
|
34
|
+
function directoriesWithGit(directories) {
|
|
35
|
+
return directories.filter((dir) => {
|
|
36
|
+
try {
|
|
37
|
+
accessSync(path.join(dir, "git"), constants.X_OK);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function gitVersionOutput() {
|
|
45
|
+
if (process.platform === "darwin") {
|
|
46
|
+
const next = gitPathForFinderLaunch(process.env.PATH, directoriesWithGit(EXTRA_GIT_DIRECTORIES));
|
|
47
|
+
if (next && next !== process.env.PATH)
|
|
48
|
+
process.env.PATH = next;
|
|
49
|
+
}
|
|
50
|
+
const { stdout } = await execFileAsync("git", ["--version"], { timeout: 15e3, windowsHide: true });
|
|
51
|
+
return stdout;
|
|
52
|
+
}
|
|
53
|
+
async function requireSystemGit(run = gitVersionOutput) {
|
|
54
|
+
let output;
|
|
55
|
+
try {
|
|
56
|
+
output = await run();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw unavailable({ zh: "\u627E\u4E0D\u5230\u53EF\u7528\u7684 git", en: "git could not be run" }, error);
|
|
59
|
+
}
|
|
60
|
+
const line = output.trim().split("\n")[0] ?? "";
|
|
61
|
+
const version = parseGitVersion(line);
|
|
62
|
+
if (!version)
|
|
63
|
+
throw unavailable({ zh: `\u65E0\u6CD5\u8BC6\u522B git \u7248\u672C\uFF08${line}\uFF09`, en: `its version is unreadable (${line})` });
|
|
64
|
+
if (!meetsMinimum(version))
|
|
65
|
+
throw unavailable({ zh: `\u5F53\u524D\u662F ${version.join(".")}`, en: `found ${version.join(".")}` });
|
|
66
|
+
return line;
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
EXTRA_GIT_DIRECTORIES,
|
|
70
|
+
FINDER_GIT_PATH,
|
|
71
|
+
MIN_GIT_VERSION,
|
|
72
|
+
gitPathForFinderLaunch,
|
|
73
|
+
parseGitVersion,
|
|
74
|
+
requireSystemGit
|
|
75
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Out } from "../direct/authCommands.js";
|
|
2
|
+
export type WpRun = {
|
|
3
|
+
nemaHome: string;
|
|
4
|
+
hubUrl: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
out: Out;
|
|
7
|
+
};
|
|
8
|
+
export type WpGetRun = WpRun & {
|
|
9
|
+
id: string;
|
|
10
|
+
dir?: string;
|
|
11
|
+
stdout?: boolean;
|
|
12
|
+
force?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function runWpList(o: WpRun): Promise<number>;
|
|
15
|
+
export declare function runWpGet(o: WpGetRun): Promise<number>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// wp/commands.js
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { repoInfo } from "../report/git.js";
|
|
5
|
+
import { getFromHub } from "../report/send.js";
|
|
6
|
+
import { ensureReportToken } from "../report/token.js";
|
|
7
|
+
async function fetchJson(o, route) {
|
|
8
|
+
const info = await repoInfo(o.cwd);
|
|
9
|
+
if (!info) {
|
|
10
|
+
o.out.error("\u4E0D\u5728 git \u4ED3\u5E93\u91CC");
|
|
11
|
+
return { ok: false, code: 1 };
|
|
12
|
+
}
|
|
13
|
+
let token;
|
|
14
|
+
try {
|
|
15
|
+
token = ensureReportToken(o.nemaHome);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
o.out.error(e instanceof Error ? e.message : String(e));
|
|
18
|
+
return { ok: false, code: 1 };
|
|
19
|
+
}
|
|
20
|
+
const sep = route.includes("?") ? "&" : "?";
|
|
21
|
+
const res = await getFromHub(o.hubUrl, `${route}${sep}gitCommonDir=${encodeURIComponent(info.gitCommonDir)}`, token);
|
|
22
|
+
if (res.status === 0 || res.status >= 500) {
|
|
23
|
+
o.out.error("Nema \u6682\u65F6\u4E0D\u53EF\u8FBE");
|
|
24
|
+
return { ok: false, code: 1 };
|
|
25
|
+
}
|
|
26
|
+
if (res.status === 204) {
|
|
27
|
+
o.out.error("\u8FD9\u4E2A\u4ED3\u5E93\u6CA1\u6709\u7ED1\u5B9A\u5230 Nema \u91CC\u4EFB\u4F55\u672A\u5F52\u6863\u7684 workspace");
|
|
28
|
+
return { ok: false, code: 1 };
|
|
29
|
+
}
|
|
30
|
+
if (res.status >= 400) {
|
|
31
|
+
let message = `Nema \u62D2\u7EDD\u4E86\u8BF7\u6C42\uFF08HTTP ${res.status}\uFF09`;
|
|
32
|
+
try {
|
|
33
|
+
const v = JSON.parse(res.text);
|
|
34
|
+
if (typeof v.error === "string")
|
|
35
|
+
message = v.error;
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
o.out.error(message);
|
|
39
|
+
return { ok: false, code: 1 };
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return { ok: true, json: JSON.parse(res.text) };
|
|
43
|
+
} catch {
|
|
44
|
+
o.out.error("Nema \u8FD4\u56DE\u4E86\u65E0\u6CD5\u89E3\u6790\u7684\u5185\u5BB9");
|
|
45
|
+
return { ok: false, code: 1 };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function runWpList(o) {
|
|
49
|
+
const r = await fetchJson(o, "/report/workpackages");
|
|
50
|
+
if (!r.ok)
|
|
51
|
+
return r.code;
|
|
52
|
+
const items = Array.isArray(r.json) ? r.json : [];
|
|
53
|
+
if (items.length === 0) {
|
|
54
|
+
o.out.log("\u5F53\u524D\u4ED3\u5E93\u6CA1\u6709\u5DF2\u6279\u51C6\u7684\u5F00\u53D1\u5305");
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
for (const w of items)
|
|
58
|
+
o.out.log(`${w.id} ${w.title} (${w.tasks} \u4E2A\u4EFB\u52A1)`);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
var reportBlock = (id) => `> \u505A\u5B8C\u6BCF\u4E2A\u4EFB\u52A1\u540E\u8FD0\u884C\uFF1Anema report --entries ${id} --task <n> --summary "\u2026" --commits <sha,\u2026> --files <path,\u2026>
|
|
62
|
+
|
|
63
|
+
`;
|
|
64
|
+
async function runWpGet(o) {
|
|
65
|
+
if (!/^W-[1-9]\d*$/.test(o.id)) {
|
|
66
|
+
o.out.error(`\u5F00\u53D1\u5305\u7F16\u53F7\u5E94\u5F62\u5982 W-2\uFF0C\u5B9E\u9645\u4E3A ${o.id}`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const r = await fetchJson(o, `/report/workpackages/${o.id}`);
|
|
70
|
+
if (!r.ok)
|
|
71
|
+
return r.code;
|
|
72
|
+
const wp = r.json;
|
|
73
|
+
const plan = reportBlock(wp.id) + wp.plan;
|
|
74
|
+
if (o.stdout) {
|
|
75
|
+
o.out.log(`${wp.spec}
|
|
76
|
+
|
|
77
|
+
${plan}`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
const base = path.join(path.resolve(o.cwd, o.dir ?? "."), ".nema", wp.id);
|
|
81
|
+
const files = [
|
|
82
|
+
{ file: path.join(base, "spec.md"), content: wp.spec },
|
|
83
|
+
{ file: path.join(base, "plan.md"), content: plan }
|
|
84
|
+
];
|
|
85
|
+
if (!o.force) {
|
|
86
|
+
for (const f of files) {
|
|
87
|
+
if (existsSync(f.file) && readFileSync(f.file, "utf8") !== f.content) {
|
|
88
|
+
o.out.error(`${f.file} \u5DF2\u5B58\u5728\u4E14\u5185\u5BB9\u4E0D\u540C\uFF1B\u52A0 --force \u8986\u76D6`);
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
mkdirSync(base, { recursive: true });
|
|
94
|
+
for (const f of files)
|
|
95
|
+
writeFileSync(f.file, f.content);
|
|
96
|
+
for (const f of files)
|
|
97
|
+
o.out.log(`\u5DF2\u5199\u5165 ${f.file}`);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
runWpGet,
|
|
102
|
+
runWpList
|
|
103
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nema-project/nema",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nema": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src/report/SKILL.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/Nema-Project/nema-room.git"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
23
|
+
"commander": "^15.0.0",
|
|
24
|
+
"smol-toml": "^1.9.1",
|
|
25
|
+
"ws": "^8.22.0",
|
|
26
|
+
"zod": "^4.6.7"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nema-report
|
|
3
|
+
description: Report finished, committed work to Nema so it can track which requirements are implemented. Use after you finish a unit of work and commit it in a repository that Nema follows.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Report finished work to Nema
|
|
7
|
+
|
|
8
|
+
When you have finished a unit of work and committed it, tell Nema what you did. Run this from inside the repository:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
nema report --summary "<one line: what you finished>" --commits <sha>[,<sha>...] --entries <id>[,<id>...] --files <path>[,<path>...]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- Report once per finished unit of work, after the commits exist. Don't report work that is unfinished or not yet committed.
|
|
15
|
+
- `--commits`: the commits that hold this work (`git log` shows them).
|
|
16
|
+
- `--entries`: reference requirement ids like R-7 when you know them. Leave the flag out when you don't.
|
|
17
|
+
- `--files`: list only files you changed, as paths relative to the repository root.
|
|
18
|
+
- Keep the summary short and factual. Never include secrets, tokens, keys or credentials anywhere in a report.
|
|
19
|
+
- If `.nema/W-*/plan.md` exists in your working tree, report each finished task with `--entries W-n --task <n>`.
|
|
20
|
+
|
|
21
|
+
If the command says Nema is unreachable, the report is kept locally and sent later. You don't need to retry. If it says the repository isn't bound to Nema, stop reporting for this repository.
|
package/README.md
DELETED