create-snipara 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/index.js +1396 -363
- package/dist/managed-config.d.ts +19 -0
- package/dist/managed-config.js +86 -0
- package/dist/managed-toolchain.d.ts +38 -0
- package/dist/managed-toolchain.js +505 -0
- package/dist/source-connection.d.ts +12 -0
- package/dist/source-connection.js +199 -0
- package/package.json +4 -3
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// src/source-connection.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import { TextDecoder } from "util";
|
|
6
|
+
|
|
7
|
+
// src/source-binding.mjs
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
lstatSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
writeFileSync
|
|
16
|
+
} from "fs";
|
|
17
|
+
import { spawnSync } from "child_process";
|
|
18
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
19
|
+
var fingerprint = (value) => createHash("sha256").update(value).digest("hex");
|
|
20
|
+
var SourceBindingError = class extends Error {
|
|
21
|
+
constructor(code, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "SourceBindingError";
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function fail(code, message) {
|
|
28
|
+
throw new SourceBindingError(code, message);
|
|
29
|
+
}
|
|
30
|
+
function normalizeRepositoryRemote(remote) {
|
|
31
|
+
if (typeof remote !== "string" || !remote || remote.length > 2048 || /[\\%\s\x00-\x1f\x7f]/.test(remote) || /\/\.{1,2}(?:\/|$)/.test(remote))
|
|
32
|
+
fail("unsafe_remote", "Repository remote is unsupported or contains unsafe credentials.");
|
|
33
|
+
let hostname, port = "", repositoryPath;
|
|
34
|
+
try {
|
|
35
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(remote)) {
|
|
36
|
+
const url = new URL(remote);
|
|
37
|
+
if (!["https:", "http:", "ssh:"].includes(url.protocol) || url.password || url.search || url.hash || url.protocol !== "ssh:" && url.username) throw new Error();
|
|
38
|
+
if (url.username && !/^[A-Za-z0-9_.-]+$/.test(url.username)) throw new Error();
|
|
39
|
+
hostname = url.hostname.toLowerCase();
|
|
40
|
+
port = url.port && !(url.protocol === "ssh:" && url.port === "22") ? `:${url.port}` : "";
|
|
41
|
+
repositoryPath = url.pathname;
|
|
42
|
+
} else {
|
|
43
|
+
const match = remote.match(/^(?:([A-Za-z0-9_.-]+)@)?([A-Za-z0-9.-]+):([^:]+)$/);
|
|
44
|
+
if (!match) throw new Error();
|
|
45
|
+
hostname = match[2].toLowerCase();
|
|
46
|
+
repositoryPath = match[3];
|
|
47
|
+
}
|
|
48
|
+
repositoryPath = repositoryPath.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
|
|
49
|
+
if (!hostname || !/^[A-Za-z0-9.-]+$/.test(hostname) || !repositoryPath || !/^[A-Za-z0-9_./~-]+$/.test(repositoryPath) || repositoryPath.split("/").some((part) => !part || part === "." || part === "..")) throw new Error();
|
|
50
|
+
} catch {
|
|
51
|
+
fail("unsafe_remote", "Repository remote is unsupported or contains unsafe credentials.");
|
|
52
|
+
}
|
|
53
|
+
if (["github.com", "gitlab.com", "bitbucket.org"].includes(hostname)) repositoryPath = repositoryPath.toLowerCase();
|
|
54
|
+
return `${hostname}${port}/${repositoryPath}`;
|
|
55
|
+
}
|
|
56
|
+
function git(cwd, args) {
|
|
57
|
+
const names = ["PATH", "HOME", "USERPROFILE", "SystemRoot", "WINDIR", "TMPDIR", "TMP", "TEMP"];
|
|
58
|
+
const env = Object.fromEntries(names.filter((key) => typeof process.env[key] === "string").map((key) => [key, process.env[key]]));
|
|
59
|
+
const result = spawnSync("git", ["--no-optional-locks", "-C", cwd, ...args], {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
timeout: 5e3,
|
|
62
|
+
maxBuffer: 16384,
|
|
63
|
+
shell: false,
|
|
64
|
+
env: { ...env, LC_ALL: "C", GIT_TERMINAL_PROMPT: "0" }
|
|
65
|
+
});
|
|
66
|
+
if (result.error || result.signal) fail("source_unavailable", "Cannot verify the current repository. Reconnect before using Snipara.");
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function observeSource(workspacePath) {
|
|
70
|
+
if (typeof workspacePath !== "string" || !workspacePath || !isAbsolute(workspacePath))
|
|
71
|
+
fail("source_unavailable", "Choose an explicit absolute workspace folder.");
|
|
72
|
+
let folder;
|
|
73
|
+
try {
|
|
74
|
+
folder = realpathSync(workspacePath);
|
|
75
|
+
if (!lstatSync(folder).isDirectory()) throw new Error();
|
|
76
|
+
} catch {
|
|
77
|
+
fail("source_unavailable", "Workspace folder is unavailable.");
|
|
78
|
+
}
|
|
79
|
+
const rootResult = git(folder, ["rev-parse", "--show-toplevel"]);
|
|
80
|
+
if (rootResult.status !== 0) {
|
|
81
|
+
if (rootResult.status !== 128 || !rootResult.stderr.includes("not a git repository"))
|
|
82
|
+
fail("source_unavailable", "Cannot verify the current repository. Reconnect before using Snipara.");
|
|
83
|
+
return { root: folder, kind: "folder", fingerprint: fingerprint(`folder:${folder}`) };
|
|
84
|
+
}
|
|
85
|
+
let root;
|
|
86
|
+
try {
|
|
87
|
+
root = realpathSync(rootResult.stdout.trim());
|
|
88
|
+
} catch {
|
|
89
|
+
fail("source_unavailable", "Repository root is unavailable.");
|
|
90
|
+
}
|
|
91
|
+
const remote = git(root, ["config", "--includes", "--get", "remote.origin.url"]);
|
|
92
|
+
if (remote.status === 0) {
|
|
93
|
+
const repository = normalizeRepositoryRemote(remote.stdout.replace(/\r?\n$/, ""));
|
|
94
|
+
return { root, kind: "repository", fingerprint: fingerprint(`repository:${repository}`), repository };
|
|
95
|
+
}
|
|
96
|
+
if (remote.status !== 1) fail("source_unavailable", "Cannot verify the repository remote.");
|
|
97
|
+
const commonResult = git(root, ["rev-parse", "--git-common-dir"]);
|
|
98
|
+
if (commonResult.status !== 0) fail("source_unavailable", "Cannot verify the local repository identity.");
|
|
99
|
+
let common;
|
|
100
|
+
try {
|
|
101
|
+
common = realpathSync(resolve(root, commonResult.stdout.trim()));
|
|
102
|
+
} catch {
|
|
103
|
+
fail("source_unavailable", "Cannot verify the local repository identity.");
|
|
104
|
+
}
|
|
105
|
+
return { root, kind: "local-repository", fingerprint: fingerprint(`local-repository:${common}`) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/source-connection.ts
|
|
109
|
+
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
110
|
+
function boundedConfiguration(file) {
|
|
111
|
+
try {
|
|
112
|
+
const parent = fs.lstatSync(path.dirname(file));
|
|
113
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== process.getuid?.())
|
|
114
|
+
throw new Error("Native configuration directory is not safely owned; preserve and repair it first");
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code === "ENOENT") return null;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
let fd;
|
|
120
|
+
try {
|
|
121
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error.code === "ENOENT") return null;
|
|
124
|
+
throw new Error("Existing native configuration is not a readable regular file; preserve and repair it first");
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const info = fs.fstatSync(fd);
|
|
128
|
+
if (!info.isFile() || info.uid !== process.getuid?.() || info.size > 1e6 || info.mode & 18)
|
|
129
|
+
throw new Error("Existing native configuration is not safely owned; preserve and repair it first");
|
|
130
|
+
const value = fs.readFileSync(fd);
|
|
131
|
+
if (value.length > 1e6) throw new Error("Native configuration exceeds its read bound");
|
|
132
|
+
return new TextDecoder("utf8", { fatal: true }).decode(value);
|
|
133
|
+
} finally {
|
|
134
|
+
fs.closeSync(fd);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function operatorPython() {
|
|
138
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
139
|
+
if (!directory || !path.isAbsolute(directory)) continue;
|
|
140
|
+
const candidate = path.join(directory, "python3");
|
|
141
|
+
try {
|
|
142
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
143
|
+
if (fs.statSync(candidate).isFile()) return fs.realpathSync(candidate);
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw new Error("Reading existing Codex configuration requires Python 3.11+, or Python 3.10 with tomli; no configuration was changed");
|
|
148
|
+
}
|
|
149
|
+
function sourceServer(value, observed) {
|
|
150
|
+
if (!object(value) || Object.keys(value).some((key) => !["command", "args"].includes(key)) || typeof value.command !== "string" || !path.isAbsolute(value.command) || !Array.isArray(value.args) || value.args.length !== 5 || !value.args.every((item) => typeof item === "string") || value.args[1] !== "--source-fingerprint" || value.args[3] !== "--source-kind" || value.args[2] !== observed.fingerprint || value.args[4] !== observed.kind || !path.isAbsolute(value.args[0]) || path.basename(value.args[0]) !== "native-mcp.mjs")
|
|
151
|
+
throw new Error("Existing Snipara repository connection differs from this workspace; reconnect it explicitly before local-work setup");
|
|
152
|
+
try {
|
|
153
|
+
const script = value.args[0], info = fs.lstatSync(script);
|
|
154
|
+
if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid?.() || info.mode & 18 || fs.realpathSync(script) !== script || info.size > 256e3 || !fs.statSync(value.command).isFile()) throw new Error();
|
|
155
|
+
} catch {
|
|
156
|
+
throw new Error("Existing Snipara repository launcher is unavailable or unsafe; preserve and repair its connection");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function detectSourceConnection(workspace, options = {}) {
|
|
160
|
+
const observed = observeSource(fs.realpathSync(workspace));
|
|
161
|
+
const servers = [];
|
|
162
|
+
const json = boundedConfiguration(path.join(observed.root, ".mcp.json"));
|
|
163
|
+
if (json !== null) {
|
|
164
|
+
let document;
|
|
165
|
+
try {
|
|
166
|
+
document = JSON.parse(json);
|
|
167
|
+
} catch {
|
|
168
|
+
throw new Error("Existing MCP JSON is malformed; preserve and repair it before setup");
|
|
169
|
+
}
|
|
170
|
+
if (!object(document) || document.mcpServers !== void 0 && !object(document.mcpServers))
|
|
171
|
+
throw new Error("Existing MCP server configuration is invalid; preserve and repair it before setup");
|
|
172
|
+
servers.push(document.mcpServers ?? {});
|
|
173
|
+
}
|
|
174
|
+
const toml = boundedConfiguration(path.join(observed.root, ".codex/config.toml"));
|
|
175
|
+
if (toml !== null) {
|
|
176
|
+
const script = "import json,sys\ntry:\n import tomllib\nexcept ImportError:\n import tomli as tomllib\nvalue=tomllib.loads(sys.stdin.read()).get('mcp_servers',{})\nif not isinstance(value,dict): raise ValueError('invalid servers')\nprint(json.dumps({key:value[key] for key in ['snipara-repository','snipara','snipara-plugin'] if key in value},ensure_ascii=True))";
|
|
177
|
+
try {
|
|
178
|
+
servers.push(JSON.parse(execFileSync(options.python ?? operatorPython(), ["-I", "-B", "-c", script], {
|
|
179
|
+
input: toml,
|
|
180
|
+
encoding: "utf8",
|
|
181
|
+
timeout: 5e3,
|
|
182
|
+
maxBuffer: 1e6,
|
|
183
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
184
|
+
env: { PATH: process.env.PATH, HOME: process.env.HOME, PYTHONNOUSERSITE: "1" }
|
|
185
|
+
})));
|
|
186
|
+
} catch {
|
|
187
|
+
throw new Error("Existing Codex configuration could not be parsed. Use Python 3.11+, or Python 3.10 with tomli, and repair malformed TOML before retrying; configuration was preserved");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const selected = servers.filter((entries) => entries["snipara-repository"] !== void 0);
|
|
191
|
+
if (!selected.length) return null;
|
|
192
|
+
if (servers.some((entries) => entries.snipara !== void 0 || entries["snipara-plugin"] !== void 0))
|
|
193
|
+
throw new Error("An existing direct Snipara connection conflicts with the repository connection; preserve and review it before setup");
|
|
194
|
+
for (const entries of selected) sourceServer(entries["snipara-repository"], observed);
|
|
195
|
+
return observed;
|
|
196
|
+
}
|
|
197
|
+
export {
|
|
198
|
+
detectSourceConnection
|
|
199
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-snipara",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Onboard Snipara Cloud or self-hosted MCP with an automatic project-grounded proof of value",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"create-snipara": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsup
|
|
12
|
-
"dev": "tsup
|
|
11
|
+
"build": "tsup --config tsup.config.ts",
|
|
12
|
+
"dev": "tsup --config tsup.config.ts --watch",
|
|
13
13
|
"prepack": "npm run build",
|
|
14
14
|
"pretest": "pnpm build",
|
|
15
15
|
"test": "node --test test/*.test.cjs",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@eslint/js": "^9.39.3",
|
|
63
|
+
"@snipara/project-intelligence-contracts": "workspace:*",
|
|
63
64
|
"@types/node": "^20.17.12",
|
|
64
65
|
"@types/prompts": "^2.4.9",
|
|
65
66
|
"@typescript-eslint/eslint-plugin": "^8.56.0",
|