@nolto/cli 0.3.0 → 0.4.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 +21 -1
- package/dist/index.js +322 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,18 @@ nolto init # Interactive setup
|
|
|
32
32
|
nolto whoami # Show current auth/config state
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
### Repo Binding
|
|
36
|
+
|
|
37
|
+
Bind the current repository to a Nolto project by writing a `nolto.json` at the repo root:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
nolto link <projectId> # Write nolto.json and commit it to share with your team
|
|
41
|
+
nolto link --show # Show the current binding (path, projectId, source)
|
|
42
|
+
nolto link --unlink # Remove the projectId key from nolto.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Once `nolto.json` is committed, every command in this repo automatically targets the correct project without needing `--project` or `NOLTO_PROJECT`.
|
|
46
|
+
|
|
35
47
|
### Project Management
|
|
36
48
|
|
|
37
49
|
```bash
|
|
@@ -91,7 +103,15 @@ Location: `~/.config/nolto/config.json` (or `$XDG_CONFIG_HOME/nolto/config.json`
|
|
|
91
103
|
| `NOLTO_BASE_URL` | Base URL (default: `https://nolto.app`) |
|
|
92
104
|
| `NOLTO_PROJECT` | Default project ID |
|
|
93
105
|
|
|
94
|
-
|
|
106
|
+
### Repo Binding File
|
|
107
|
+
|
|
108
|
+
`nolto.json` at the repository root pins the project for everyone who clones the repo:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{ "projectId": "00000000-0000-0000-0000-000000000001" }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Precedence**: CLI flags > environment variables > `nolto.json` (repo binding) > config file > defaults
|
|
95
115
|
|
|
96
116
|
## Exit Codes
|
|
97
117
|
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { createRequire as createRequire2 } from "module";
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
|
-
import
|
|
6
|
+
import path7 from "path";
|
|
7
7
|
import { CommanderError } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
10
|
import { readFile, writeFile, mkdir, stat } from "fs/promises";
|
|
11
|
+
import { statSync } from "fs";
|
|
11
12
|
import os from "os";
|
|
12
13
|
import path from "path";
|
|
13
14
|
import { z } from "zod";
|
|
@@ -85,6 +86,79 @@ var configSchema = z.object({
|
|
|
85
86
|
baseUrl: z.string().url().optional(),
|
|
86
87
|
defaultProjectId: z.string().uuid().optional()
|
|
87
88
|
}).strict();
|
|
89
|
+
var repoBindingSchema = z.object({ projectId: z.string().uuid() }).passthrough();
|
|
90
|
+
async function loadRepoBinding(filePath) {
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = await readFile(filePath, "utf8");
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const code = err != null && typeof err === "object" && "code" in err ? err.code : "";
|
|
96
|
+
if (code === "ENOENT") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
throw new CliError(`Cannot read repo binding file: ${filePath}: ${String(err)}`, 2);
|
|
100
|
+
}
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(raw);
|
|
104
|
+
} catch {
|
|
105
|
+
throw new CliError(`Malformed JSON in ${filePath}`, 2);
|
|
106
|
+
}
|
|
107
|
+
const result = repoBindingSchema.safeParse(parsed);
|
|
108
|
+
if (!result.success) {
|
|
109
|
+
const issue = result.error.issues[0];
|
|
110
|
+
const fieldPath = issue?.path.join(".") ?? "projectId";
|
|
111
|
+
throw new CliError(
|
|
112
|
+
`Invalid repo binding at ${filePath}: field "${fieldPath}" \u2014 ${issue?.message ?? "validation failed"}`,
|
|
113
|
+
2
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return result.data;
|
|
117
|
+
}
|
|
118
|
+
function findNoltoJsonSync(startDir) {
|
|
119
|
+
let current = startDir;
|
|
120
|
+
while (true) {
|
|
121
|
+
const candidate = path.join(current, "nolto.json");
|
|
122
|
+
try {
|
|
123
|
+
statSync(candidate);
|
|
124
|
+
return candidate;
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
const parent = path.dirname(current);
|
|
128
|
+
if (parent === current) break;
|
|
129
|
+
current = parent;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function findRepoBindingFile(args) {
|
|
134
|
+
const cpd = args.env["CLAUDE_PROJECT_DIR"];
|
|
135
|
+
const startDir = cpd != null && cpd.length > 0 ? cpd : args.cwd;
|
|
136
|
+
return findNoltoJsonSync(startDir);
|
|
137
|
+
}
|
|
138
|
+
async function writeRepoBinding(root, projectId) {
|
|
139
|
+
const filePath = path.join(root, "nolto.json");
|
|
140
|
+
let existing = {};
|
|
141
|
+
try {
|
|
142
|
+
const raw = await readFile(filePath, "utf8");
|
|
143
|
+
const parsed = JSON.parse(raw);
|
|
144
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
145
|
+
throw new CliError(
|
|
146
|
+
`Cannot update ${filePath}: file contains valid JSON but is not a plain object (got ${Array.isArray(parsed) ? "array" : String(parsed)}). Remove or fix the file before linking.`,
|
|
147
|
+
2
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
existing = parsed;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (err instanceof CliError) throw err;
|
|
153
|
+
const code = err != null && typeof err === "object" && "code" in err ? err.code : "";
|
|
154
|
+
if (code !== "ENOENT") {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const merged = { ...existing, projectId };
|
|
158
|
+
const { chmod } = await import("fs/promises");
|
|
159
|
+
await writeFile(filePath, JSON.stringify(merged, null, 2) + "\n", { mode: 420 });
|
|
160
|
+
await chmod(filePath, 420);
|
|
161
|
+
}
|
|
88
162
|
function getConfigDir(env) {
|
|
89
163
|
const xdg = env["XDG_CONFIG_HOME"];
|
|
90
164
|
const base = xdg != null && xdg.length > 0 ? xdg : path.join(os.homedir(), ".config");
|
|
@@ -138,8 +212,24 @@ async function saveConfigFile(filePath, value) {
|
|
|
138
212
|
await mkdir(path.dirname(filePath), { recursive: true, mode: 448 });
|
|
139
213
|
await writeFile(filePath, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
140
214
|
}
|
|
215
|
+
function resolveProjectId(args) {
|
|
216
|
+
const { flagProject, envProject, repoBinding, fileProjectId } = args;
|
|
217
|
+
if (flagProject != null && flagProject.length > 0) {
|
|
218
|
+
return { defaultProjectId: flagProject, projectSource: "flag" };
|
|
219
|
+
}
|
|
220
|
+
if (envProject != null && envProject.length > 0) {
|
|
221
|
+
return { defaultProjectId: envProject, projectSource: "env" };
|
|
222
|
+
}
|
|
223
|
+
if (repoBinding != null && repoBinding.projectId.length > 0) {
|
|
224
|
+
return { defaultProjectId: repoBinding.projectId, projectSource: "repo" };
|
|
225
|
+
}
|
|
226
|
+
if (fileProjectId != null && fileProjectId.length > 0) {
|
|
227
|
+
return { defaultProjectId: fileProjectId, projectSource: "file" };
|
|
228
|
+
}
|
|
229
|
+
return { defaultProjectId: void 0, projectSource: "none" };
|
|
230
|
+
}
|
|
141
231
|
function resolveSettings(args) {
|
|
142
|
-
const { flags, env, file } = args;
|
|
232
|
+
const { flags, env, file, repoBinding } = args;
|
|
143
233
|
let token;
|
|
144
234
|
let tokenSource = "none";
|
|
145
235
|
if (flags.token != null && flags.token.length > 0) {
|
|
@@ -166,18 +256,12 @@ function resolveSettings(args) {
|
|
|
166
256
|
} else {
|
|
167
257
|
baseUrl = DEFAULT_BASE_URL;
|
|
168
258
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
defaultProjectId = env["NOLTO_PROJECT"];
|
|
176
|
-
projectSource = "env";
|
|
177
|
-
} else if (file?.defaultProjectId != null && file.defaultProjectId.length > 0) {
|
|
178
|
-
defaultProjectId = file.defaultProjectId;
|
|
179
|
-
projectSource = "file";
|
|
180
|
-
}
|
|
259
|
+
const { defaultProjectId, projectSource } = resolveProjectId({
|
|
260
|
+
flagProject: flags.project,
|
|
261
|
+
envProject: env["NOLTO_PROJECT"],
|
|
262
|
+
repoBinding,
|
|
263
|
+
fileProjectId: file?.defaultProjectId
|
|
264
|
+
});
|
|
181
265
|
return {
|
|
182
266
|
token,
|
|
183
267
|
baseUrl,
|
|
@@ -419,11 +503,11 @@ function createHttpClient(opts) {
|
|
|
419
503
|
const { baseUrl, version, token } = opts;
|
|
420
504
|
const base = baseUrl.replace(/\/+$/, "");
|
|
421
505
|
return {
|
|
422
|
-
async post(
|
|
423
|
-
if (!
|
|
424
|
-
throw new CliError(`HTTP client path must start with /api/, got: ${
|
|
506
|
+
async post(path8, body) {
|
|
507
|
+
if (!path8.startsWith("/api/")) {
|
|
508
|
+
throw new CliError(`HTTP client path must start with /api/, got: ${path8}`, 2);
|
|
425
509
|
}
|
|
426
|
-
const url = `${base}${
|
|
510
|
+
const url = `${base}${path8}`;
|
|
427
511
|
const headers = {
|
|
428
512
|
"Content-Type": "application/json",
|
|
429
513
|
"User-Agent": `${CLI_USER_AGENT_NAME}/${version}`
|
|
@@ -581,9 +665,43 @@ async function injectCursor(opts) {
|
|
|
581
665
|
`);
|
|
582
666
|
}
|
|
583
667
|
async function injectClaude(opts) {
|
|
668
|
+
const mcpUrl = `${opts.baseUrl.replace(/\/+$/, "")}/mcp`;
|
|
669
|
+
const claudePath = await findOnPath("claude");
|
|
670
|
+
if (claudePath) {
|
|
671
|
+
const execFileAsync2 = getExecFileAsync();
|
|
672
|
+
await execFileAsync2(claudePath, ["mcp", "remove", "nolto", "--scope", "user"]).catch(
|
|
673
|
+
() => void 0
|
|
674
|
+
);
|
|
675
|
+
try {
|
|
676
|
+
const { stderr } = await execFileAsync2(claudePath, [
|
|
677
|
+
"mcp",
|
|
678
|
+
"add",
|
|
679
|
+
"nolto",
|
|
680
|
+
mcpUrl,
|
|
681
|
+
"--transport",
|
|
682
|
+
"http",
|
|
683
|
+
"--scope",
|
|
684
|
+
"user",
|
|
685
|
+
"--header",
|
|
686
|
+
`Authorization: Bearer ${opts.token}`
|
|
687
|
+
]);
|
|
688
|
+
if (stderr) {
|
|
689
|
+
process.stderr.write(`[claude mcp add] ${stderr}
|
|
690
|
+
`);
|
|
691
|
+
}
|
|
692
|
+
process.stdout.write("Registered nolto MCP server in Claude Code (user scope).\n");
|
|
693
|
+
process.stdout.write("Reconnect or restart Claude Code to use it.\n");
|
|
694
|
+
return;
|
|
695
|
+
} catch (err) {
|
|
696
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
697
|
+
process.stderr.write(
|
|
698
|
+
`Warning: claude mcp add failed (${msg}); writing project .mcp.json instead.
|
|
699
|
+
`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
584
703
|
const cwd = opts.claudeCwd ?? process.cwd();
|
|
585
704
|
const configPath = path3.join(cwd, ".mcp.json");
|
|
586
|
-
const mcpUrl = `${opts.baseUrl.replace(/\/+$/, "")}/mcp`;
|
|
587
705
|
const result = await readJsonConfig(configPath);
|
|
588
706
|
const existing = result?.parsed ?? {};
|
|
589
707
|
const updatedServers = {
|
|
@@ -861,6 +979,7 @@ function register3(program, deps) {
|
|
|
861
979
|
} catch {
|
|
862
980
|
}
|
|
863
981
|
}
|
|
982
|
+
const projectBindingPath = deps.projectBindingPath ?? null;
|
|
864
983
|
if (mode2 === "json") {
|
|
865
984
|
printResult(
|
|
866
985
|
{
|
|
@@ -871,17 +990,19 @@ function register3(program, deps) {
|
|
|
871
990
|
tokenSource: settings.source.token,
|
|
872
991
|
defaultProject: settings.defaultProjectId ?? null,
|
|
873
992
|
defaultProjectSource: settings.source.project,
|
|
993
|
+
projectBindingPath: projectBindingPath ?? null,
|
|
874
994
|
projects: projectCount ?? null
|
|
875
995
|
},
|
|
876
996
|
mode2
|
|
877
997
|
);
|
|
878
998
|
} else {
|
|
879
999
|
const tokenDisplay = settings.token != null ? `${maskToken(settings.token)} (source: ${settings.source.token})` : "(not configured)";
|
|
1000
|
+
const bindingDisplay = settings.source.project === "repo" && projectBindingPath != null ? `${settings.defaultProjectId ?? "none"} (source: repo, binding: ${projectBindingPath})` : `${settings.defaultProjectId ?? "none"} (source: ${settings.source.project})`;
|
|
880
1001
|
const lines = [
|
|
881
1002
|
["baseUrl", `${settings.baseUrl} (source: ${settings.source.baseUrl})`],
|
|
882
1003
|
["configPath", configPath],
|
|
883
1004
|
["token", tokenDisplay],
|
|
884
|
-
["defaultProject",
|
|
1005
|
+
["defaultProject", bindingDisplay],
|
|
885
1006
|
["projects", projectCount != null ? String(projectCount) : settings.token != null ? "(unavailable)" : "(no token)"]
|
|
886
1007
|
];
|
|
887
1008
|
const maxKey = lines.reduce((m, [k]) => Math.max(m, k.length), 0);
|
|
@@ -1405,7 +1526,7 @@ function register7(program, deps) {
|
|
|
1405
1526
|
}
|
|
1406
1527
|
|
|
1407
1528
|
// src/queue-file.ts
|
|
1408
|
-
import { openSync, closeSync, writeFileSync, unlinkSync, renameSync, statSync } from "fs";
|
|
1529
|
+
import { openSync, closeSync, writeFileSync, unlinkSync, renameSync, statSync as statSync2 } from "fs";
|
|
1409
1530
|
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3, unlink as unlink2, appendFile } from "fs/promises";
|
|
1410
1531
|
import path5 from "path";
|
|
1411
1532
|
import crypto from "crypto";
|
|
@@ -1438,7 +1559,7 @@ function findAncestorWithMarker(startDir) {
|
|
|
1438
1559
|
}
|
|
1439
1560
|
function hasMarkerSync(dir, marker) {
|
|
1440
1561
|
try {
|
|
1441
|
-
|
|
1562
|
+
statSync2(path5.join(dir, marker));
|
|
1442
1563
|
return true;
|
|
1443
1564
|
} catch {
|
|
1444
1565
|
return false;
|
|
@@ -1813,6 +1934,170 @@ function register8(program, deps) {
|
|
|
1813
1934
|
});
|
|
1814
1935
|
}
|
|
1815
1936
|
|
|
1937
|
+
// src/commands/link.ts
|
|
1938
|
+
import path6 from "path";
|
|
1939
|
+
import { statSync as statSync3 } from "fs";
|
|
1940
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1941
|
+
function resolveStartDir(env, cwd) {
|
|
1942
|
+
const cpd = env["CLAUDE_PROJECT_DIR"];
|
|
1943
|
+
return cpd != null && cpd.length > 0 ? cpd : cwd;
|
|
1944
|
+
}
|
|
1945
|
+
function dirHasGit(dir) {
|
|
1946
|
+
try {
|
|
1947
|
+
statSync3(path6.join(dir, ".git"));
|
|
1948
|
+
return true;
|
|
1949
|
+
} catch {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
function findRepoRoot(startDir, hasGit = dirHasGit) {
|
|
1954
|
+
let current = startDir;
|
|
1955
|
+
while (true) {
|
|
1956
|
+
if (hasGit(current)) {
|
|
1957
|
+
return { root: current, foundGit: true };
|
|
1958
|
+
}
|
|
1959
|
+
const parent = path6.dirname(current);
|
|
1960
|
+
if (parent === current) break;
|
|
1961
|
+
current = parent;
|
|
1962
|
+
}
|
|
1963
|
+
return { root: startDir, foundGit: false };
|
|
1964
|
+
}
|
|
1965
|
+
async function handleShow(deps, projectBindingPath, mode2) {
|
|
1966
|
+
if (projectBindingPath == null) {
|
|
1967
|
+
if (mode2 === "json") {
|
|
1968
|
+
printResult({ bound: false, projectBindingPath: null }, mode2);
|
|
1969
|
+
} else {
|
|
1970
|
+
process.stdout.write("No nolto.json binding found in this directory tree.\n");
|
|
1971
|
+
}
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
const binding = await loadRepoBinding(projectBindingPath).catch((err) => {
|
|
1975
|
+
if (err instanceof CliError) throw err;
|
|
1976
|
+
throw new CliError(`Cannot read binding: ${String(err)}`, 2);
|
|
1977
|
+
});
|
|
1978
|
+
if (mode2 === "json") {
|
|
1979
|
+
printResult({
|
|
1980
|
+
bound: binding != null,
|
|
1981
|
+
projectId: binding?.projectId ?? null,
|
|
1982
|
+
projectBindingPath,
|
|
1983
|
+
source: deps.settings.source.project === "repo" ? "repo" : "file"
|
|
1984
|
+
}, mode2);
|
|
1985
|
+
} else {
|
|
1986
|
+
if (binding == null) {
|
|
1987
|
+
process.stdout.write(`Binding file found at ${projectBindingPath} but could not be read.
|
|
1988
|
+
`);
|
|
1989
|
+
} else {
|
|
1990
|
+
process.stdout.write(`Binding file : ${projectBindingPath}
|
|
1991
|
+
`);
|
|
1992
|
+
process.stdout.write(`projectId : ${binding.projectId}
|
|
1993
|
+
`);
|
|
1994
|
+
const active = deps.settings.source.project === "repo" ? "repo (active)" : "repo (not active \u2014 overridden)";
|
|
1995
|
+
process.stdout.write(`source : ${active}
|
|
1996
|
+
`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
async function handleUnlink(projectBindingPath, mode2) {
|
|
2001
|
+
const { readFile: readFile5, writeFile: writeFile4, chmod } = await import("fs/promises");
|
|
2002
|
+
let existing = {};
|
|
2003
|
+
try {
|
|
2004
|
+
const raw = await readFile5(projectBindingPath, "utf8");
|
|
2005
|
+
const parsed = JSON.parse(raw);
|
|
2006
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2007
|
+
throw new CliError(
|
|
2008
|
+
`Cannot unlink ${projectBindingPath}: file contains valid JSON but is not a plain object (got ${Array.isArray(parsed) ? "array" : String(parsed)}). Remove or fix the file manually.`,
|
|
2009
|
+
2
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
existing = parsed;
|
|
2013
|
+
} catch (err) {
|
|
2014
|
+
if (err instanceof CliError) throw err;
|
|
2015
|
+
throw new CliError(`Cannot read ${projectBindingPath}: ${String(err)}`, 2);
|
|
2016
|
+
}
|
|
2017
|
+
const { projectId: _removed, ...rest } = existing;
|
|
2018
|
+
void _removed;
|
|
2019
|
+
await writeFile4(projectBindingPath, JSON.stringify(rest, null, 2) + "\n", { mode: 420 });
|
|
2020
|
+
await chmod(projectBindingPath, 420);
|
|
2021
|
+
if (mode2 === "json") {
|
|
2022
|
+
printResult({ unlinked: true, projectBindingPath }, mode2);
|
|
2023
|
+
} else {
|
|
2024
|
+
process.stdout.write(`Removed projectId from ${projectBindingPath}.
|
|
2025
|
+
`);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
async function performLink(deps, projectId, mode2) {
|
|
2029
|
+
if (!UUID_RE.test(projectId)) {
|
|
2030
|
+
throw new CliError(
|
|
2031
|
+
`Invalid project ID: "${projectId}". Must be a UUID (e.g. 00000000-0000-0000-0000-000000000001).`,
|
|
2032
|
+
2
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
const startDir = resolveStartDir(process.env, process.cwd());
|
|
2036
|
+
const { root, foundGit } = findRepoRoot(startDir);
|
|
2037
|
+
if (!foundGit) {
|
|
2038
|
+
process.stderr.write(
|
|
2039
|
+
`Warning: no .git directory found above ${startDir}. Writing nolto.json to current directory.
|
|
2040
|
+
`
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
if (deps.settings.token != null) {
|
|
2044
|
+
try {
|
|
2045
|
+
const result = await deps.caller.call("list_projects", {});
|
|
2046
|
+
const projects = result?.projects ?? [];
|
|
2047
|
+
if (!projects.some((p) => p.id === projectId)) {
|
|
2048
|
+
process.stderr.write(
|
|
2049
|
+
`Warning: project ${projectId} was not found in your list_projects response.
|
|
2050
|
+
Proceeding anyway \u2014 verify the ID is correct.
|
|
2051
|
+
`
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
} catch {
|
|
2055
|
+
process.stderr.write(
|
|
2056
|
+
"Warning: could not verify project membership (offline or token issue). Proceeding anyway.\n"
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
await writeRepoBinding(root, projectId);
|
|
2061
|
+
const writtenPath = path6.join(root, "nolto.json");
|
|
2062
|
+
if (mode2 === "json") {
|
|
2063
|
+
printResult({ linked: true, projectId, projectBindingPath: writtenPath }, mode2);
|
|
2064
|
+
} else {
|
|
2065
|
+
process.stdout.write(
|
|
2066
|
+
`Linked this repo to project ${projectId} (wrote ${writtenPath}).
|
|
2067
|
+
Commit nolto.json to share the binding with your team.
|
|
2068
|
+
`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
function register9(program, deps) {
|
|
2073
|
+
const cmd = program.command("link [projectId]").description(
|
|
2074
|
+
"Bind this repository to a Nolto project.\nWrites nolto.json at the repo root. Commit it to share the binding with your team.\n\nExamples:\n nolto link <uuid> Write / update nolto.json\n nolto link --show Show the current binding\n nolto link --unlink Remove the projectId from nolto.json"
|
|
2075
|
+
).option("--show", "Show the current repo binding (path + projectId + source)").option("--unlink", "Remove the projectId key from nolto.json");
|
|
2076
|
+
cmd.action(async (projectId) => {
|
|
2077
|
+
const { output } = deps;
|
|
2078
|
+
const projectBindingPath = deps.projectBindingPath ?? null;
|
|
2079
|
+
const mode2 = output.mode;
|
|
2080
|
+
if (cmd.opts()["show"]) {
|
|
2081
|
+
await handleShow(deps, projectBindingPath, mode2);
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
if (cmd.opts()["unlink"]) {
|
|
2085
|
+
if (projectBindingPath == null) {
|
|
2086
|
+
throw new CliError("No nolto.json found in this directory tree. Nothing to unlink.", 2);
|
|
2087
|
+
}
|
|
2088
|
+
await handleUnlink(projectBindingPath, mode2);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
if (projectId == null || projectId.trim().length === 0) {
|
|
2092
|
+
throw new CliError(
|
|
2093
|
+
"Usage: nolto link <projectId> (provide a UUID)\nOr use --show to view the current binding, --unlink to remove it.",
|
|
2094
|
+
2
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
await performLink(deps, projectId, mode2);
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
|
|
1816
2101
|
// src/program.ts
|
|
1817
2102
|
function stripCommanderErrorPrefix(msg) {
|
|
1818
2103
|
return msg.startsWith("error: ") ? msg.slice("error: ".length) : msg;
|
|
@@ -1830,15 +2115,16 @@ function buildProgram(deps) {
|
|
|
1830
2115
|
register7(program, deps);
|
|
1831
2116
|
registerQueue(program, deps);
|
|
1832
2117
|
register8(program, deps);
|
|
2118
|
+
register9(program, deps);
|
|
1833
2119
|
return program;
|
|
1834
2120
|
}
|
|
1835
2121
|
|
|
1836
2122
|
// src/index.ts
|
|
1837
|
-
var __dirname2 =
|
|
2123
|
+
var __dirname2 = path7.dirname(fileURLToPath2(import.meta.url));
|
|
1838
2124
|
var require2 = createRequire2(import.meta.url);
|
|
1839
2125
|
function getVersion() {
|
|
1840
2126
|
try {
|
|
1841
|
-
const pkgPath =
|
|
2127
|
+
const pkgPath = path7.resolve(__dirname2, "../package.json");
|
|
1842
2128
|
const pkg = require2(pkgPath);
|
|
1843
2129
|
return pkg.version ?? "0.0.0";
|
|
1844
2130
|
} catch {
|
|
@@ -1871,10 +2157,20 @@ async function main() {
|
|
|
1871
2157
|
process.exit();
|
|
1872
2158
|
}
|
|
1873
2159
|
}
|
|
2160
|
+
const projectBindingPath = findRepoBindingFile({ env: process.env, cwd: process.cwd() });
|
|
2161
|
+
const repoBinding = projectBindingPath != null ? await loadRepoBinding(projectBindingPath).catch((err) => {
|
|
2162
|
+
if (err instanceof CliError) {
|
|
2163
|
+
printError(err, mode);
|
|
2164
|
+
process.exitCode = err.exitCode;
|
|
2165
|
+
process.exit();
|
|
2166
|
+
}
|
|
2167
|
+
throw err;
|
|
2168
|
+
}) : null;
|
|
1874
2169
|
const settings = resolveSettings({
|
|
1875
2170
|
flags: { token: flagToken, baseUrl: flagBaseUrl, project: flagProject },
|
|
1876
2171
|
env: process.env,
|
|
1877
|
-
file: configFile
|
|
2172
|
+
file: configFile,
|
|
2173
|
+
repoBinding
|
|
1878
2174
|
});
|
|
1879
2175
|
const version = getVersion();
|
|
1880
2176
|
const caller = settings.token != null ? createMcpCaller({ baseUrl: settings.baseUrl, token: settings.token, version }) : {
|
|
@@ -1885,7 +2181,7 @@ async function main() {
|
|
|
1885
2181
|
);
|
|
1886
2182
|
}
|
|
1887
2183
|
};
|
|
1888
|
-
const program = buildProgram({ caller, settings, output: { mode }, version, configPath });
|
|
2184
|
+
const program = buildProgram({ caller, settings, output: { mode }, version, configPath, projectBindingPath });
|
|
1889
2185
|
await program.parseAsync(process.argv);
|
|
1890
2186
|
}
|
|
1891
2187
|
function extractFlag(argv, name) {
|