@nolto/cli 0.3.1 → 0.5.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 +394 -30
- 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 path8 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(path9, body) {
|
|
507
|
+
if (!path9.startsWith("/api/")) {
|
|
508
|
+
throw new CliError(`HTTP client path must start with /api/, got: ${path9}`, 2);
|
|
425
509
|
}
|
|
426
|
-
const url = `${base}${
|
|
510
|
+
const url = `${base}${path9}`;
|
|
427
511
|
const headers = {
|
|
428
512
|
"Content-Type": "application/json",
|
|
429
513
|
"User-Agent": `${CLI_USER_AGENT_NAME}/${version}`
|
|
@@ -625,10 +709,7 @@ async function injectClaude(opts) {
|
|
|
625
709
|
nolto: {
|
|
626
710
|
url: mcpUrl,
|
|
627
711
|
headers: {
|
|
628
|
-
Authorization:
|
|
629
|
-
},
|
|
630
|
-
env: {
|
|
631
|
-
NOLTO_MCP_TOKEN: opts.token
|
|
712
|
+
Authorization: "Bearer ${NOLTO_MCP_TOKEN}"
|
|
632
713
|
}
|
|
633
714
|
}
|
|
634
715
|
};
|
|
@@ -637,7 +718,14 @@ async function injectClaude(opts) {
|
|
|
637
718
|
mcpServers: updatedServers
|
|
638
719
|
};
|
|
639
720
|
await atomicWrite(configPath, JSON.stringify(newConfig, null, 2) + "\n", 384);
|
|
640
|
-
|
|
721
|
+
const tokensUrl = `${opts.baseUrl.replace(/\/+$/, "")}/settings/tokens`;
|
|
722
|
+
process.stdout.write(
|
|
723
|
+
`Updated ${configPath} (claude project .mcp.json) \u2014 uses the NOLTO_MCP_TOKEN env var; no token is stored in the file, so it is safe to commit.
|
|
724
|
+
`
|
|
725
|
+
);
|
|
726
|
+
process.stdout.write("Set NOLTO_MCP_TOKEN in your environment before launching Claude Code, e.g.:\n");
|
|
727
|
+
process.stdout.write(" export NOLTO_MCP_TOKEN=<your token>\n");
|
|
728
|
+
process.stdout.write(`Your token is saved in your nolto config (from this login); or create one at ${tokensUrl}
|
|
641
729
|
`);
|
|
642
730
|
}
|
|
643
731
|
async function injectCodex(opts) {
|
|
@@ -895,6 +983,7 @@ function register3(program, deps) {
|
|
|
895
983
|
} catch {
|
|
896
984
|
}
|
|
897
985
|
}
|
|
986
|
+
const projectBindingPath = deps.projectBindingPath ?? null;
|
|
898
987
|
if (mode2 === "json") {
|
|
899
988
|
printResult(
|
|
900
989
|
{
|
|
@@ -905,17 +994,19 @@ function register3(program, deps) {
|
|
|
905
994
|
tokenSource: settings.source.token,
|
|
906
995
|
defaultProject: settings.defaultProjectId ?? null,
|
|
907
996
|
defaultProjectSource: settings.source.project,
|
|
997
|
+
projectBindingPath: projectBindingPath ?? null,
|
|
908
998
|
projects: projectCount ?? null
|
|
909
999
|
},
|
|
910
1000
|
mode2
|
|
911
1001
|
);
|
|
912
1002
|
} else {
|
|
913
1003
|
const tokenDisplay = settings.token != null ? `${maskToken(settings.token)} (source: ${settings.source.token})` : "(not configured)";
|
|
1004
|
+
const bindingDisplay = settings.source.project === "repo" && projectBindingPath != null ? `${settings.defaultProjectId ?? "none"} (source: repo, binding: ${projectBindingPath})` : `${settings.defaultProjectId ?? "none"} (source: ${settings.source.project})`;
|
|
914
1005
|
const lines = [
|
|
915
1006
|
["baseUrl", `${settings.baseUrl} (source: ${settings.source.baseUrl})`],
|
|
916
1007
|
["configPath", configPath],
|
|
917
1008
|
["token", tokenDisplay],
|
|
918
|
-
["defaultProject",
|
|
1009
|
+
["defaultProject", bindingDisplay],
|
|
919
1010
|
["projects", projectCount != null ? String(projectCount) : settings.token != null ? "(unavailable)" : "(no token)"]
|
|
920
1011
|
];
|
|
921
1012
|
const maxKey = lines.reduce((m, [k]) => Math.max(m, k.length), 0);
|
|
@@ -1439,7 +1530,7 @@ function register7(program, deps) {
|
|
|
1439
1530
|
}
|
|
1440
1531
|
|
|
1441
1532
|
// src/queue-file.ts
|
|
1442
|
-
import { openSync, closeSync, writeFileSync, unlinkSync, renameSync, statSync } from "fs";
|
|
1533
|
+
import { openSync, closeSync, writeFileSync, unlinkSync, renameSync, statSync as statSync2 } from "fs";
|
|
1443
1534
|
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3, unlink as unlink2, appendFile } from "fs/promises";
|
|
1444
1535
|
import path5 from "path";
|
|
1445
1536
|
import crypto from "crypto";
|
|
@@ -1472,7 +1563,7 @@ function findAncestorWithMarker(startDir) {
|
|
|
1472
1563
|
}
|
|
1473
1564
|
function hasMarkerSync(dir, marker) {
|
|
1474
1565
|
try {
|
|
1475
|
-
|
|
1566
|
+
statSync2(path5.join(dir, marker));
|
|
1476
1567
|
return true;
|
|
1477
1568
|
} catch {
|
|
1478
1569
|
return false;
|
|
@@ -1847,6 +1938,170 @@ function register8(program, deps) {
|
|
|
1847
1938
|
});
|
|
1848
1939
|
}
|
|
1849
1940
|
|
|
1941
|
+
// src/commands/link.ts
|
|
1942
|
+
import path6 from "path";
|
|
1943
|
+
import { statSync as statSync3 } from "fs";
|
|
1944
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1945
|
+
function resolveStartDir(env, cwd) {
|
|
1946
|
+
const cpd = env["CLAUDE_PROJECT_DIR"];
|
|
1947
|
+
return cpd != null && cpd.length > 0 ? cpd : cwd;
|
|
1948
|
+
}
|
|
1949
|
+
function dirHasGit(dir) {
|
|
1950
|
+
try {
|
|
1951
|
+
statSync3(path6.join(dir, ".git"));
|
|
1952
|
+
return true;
|
|
1953
|
+
} catch {
|
|
1954
|
+
return false;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
function findRepoRoot(startDir, hasGit = dirHasGit) {
|
|
1958
|
+
let current = startDir;
|
|
1959
|
+
while (true) {
|
|
1960
|
+
if (hasGit(current)) {
|
|
1961
|
+
return { root: current, foundGit: true };
|
|
1962
|
+
}
|
|
1963
|
+
const parent = path6.dirname(current);
|
|
1964
|
+
if (parent === current) break;
|
|
1965
|
+
current = parent;
|
|
1966
|
+
}
|
|
1967
|
+
return { root: startDir, foundGit: false };
|
|
1968
|
+
}
|
|
1969
|
+
async function handleShow(deps, projectBindingPath, mode2) {
|
|
1970
|
+
if (projectBindingPath == null) {
|
|
1971
|
+
if (mode2 === "json") {
|
|
1972
|
+
printResult({ bound: false, projectBindingPath: null }, mode2);
|
|
1973
|
+
} else {
|
|
1974
|
+
process.stdout.write("No nolto.json binding found in this directory tree.\n");
|
|
1975
|
+
}
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
const binding = await loadRepoBinding(projectBindingPath).catch((err) => {
|
|
1979
|
+
if (err instanceof CliError) throw err;
|
|
1980
|
+
throw new CliError(`Cannot read binding: ${String(err)}`, 2);
|
|
1981
|
+
});
|
|
1982
|
+
if (mode2 === "json") {
|
|
1983
|
+
printResult({
|
|
1984
|
+
bound: binding != null,
|
|
1985
|
+
projectId: binding?.projectId ?? null,
|
|
1986
|
+
projectBindingPath,
|
|
1987
|
+
source: deps.settings.source.project === "repo" ? "repo" : "file"
|
|
1988
|
+
}, mode2);
|
|
1989
|
+
} else {
|
|
1990
|
+
if (binding == null) {
|
|
1991
|
+
process.stdout.write(`Binding file found at ${projectBindingPath} but could not be read.
|
|
1992
|
+
`);
|
|
1993
|
+
} else {
|
|
1994
|
+
process.stdout.write(`Binding file : ${projectBindingPath}
|
|
1995
|
+
`);
|
|
1996
|
+
process.stdout.write(`projectId : ${binding.projectId}
|
|
1997
|
+
`);
|
|
1998
|
+
const active = deps.settings.source.project === "repo" ? "repo (active)" : "repo (not active \u2014 overridden)";
|
|
1999
|
+
process.stdout.write(`source : ${active}
|
|
2000
|
+
`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
async function handleUnlink(projectBindingPath, mode2) {
|
|
2005
|
+
const { readFile: readFile6, writeFile: writeFile5, chmod } = await import("fs/promises");
|
|
2006
|
+
let existing = {};
|
|
2007
|
+
try {
|
|
2008
|
+
const raw = await readFile6(projectBindingPath, "utf8");
|
|
2009
|
+
const parsed = JSON.parse(raw);
|
|
2010
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2011
|
+
throw new CliError(
|
|
2012
|
+
`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.`,
|
|
2013
|
+
2
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
existing = parsed;
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
if (err instanceof CliError) throw err;
|
|
2019
|
+
throw new CliError(`Cannot read ${projectBindingPath}: ${String(err)}`, 2);
|
|
2020
|
+
}
|
|
2021
|
+
const { projectId: _removed, ...rest } = existing;
|
|
2022
|
+
void _removed;
|
|
2023
|
+
await writeFile5(projectBindingPath, JSON.stringify(rest, null, 2) + "\n", { mode: 420 });
|
|
2024
|
+
await chmod(projectBindingPath, 420);
|
|
2025
|
+
if (mode2 === "json") {
|
|
2026
|
+
printResult({ unlinked: true, projectBindingPath }, mode2);
|
|
2027
|
+
} else {
|
|
2028
|
+
process.stdout.write(`Removed projectId from ${projectBindingPath}.
|
|
2029
|
+
`);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
async function performLink(deps, projectId, mode2) {
|
|
2033
|
+
if (!UUID_RE.test(projectId)) {
|
|
2034
|
+
throw new CliError(
|
|
2035
|
+
`Invalid project ID: "${projectId}". Must be a UUID (e.g. 00000000-0000-0000-0000-000000000001).`,
|
|
2036
|
+
2
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
const startDir = resolveStartDir(process.env, process.cwd());
|
|
2040
|
+
const { root, foundGit } = findRepoRoot(startDir);
|
|
2041
|
+
if (!foundGit) {
|
|
2042
|
+
process.stderr.write(
|
|
2043
|
+
`Warning: no .git directory found above ${startDir}. Writing nolto.json to current directory.
|
|
2044
|
+
`
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
if (deps.settings.token != null) {
|
|
2048
|
+
try {
|
|
2049
|
+
const result = await deps.caller.call("list_projects", {});
|
|
2050
|
+
const projects = result?.projects ?? [];
|
|
2051
|
+
if (!projects.some((p) => p.id === projectId)) {
|
|
2052
|
+
process.stderr.write(
|
|
2053
|
+
`Warning: project ${projectId} was not found in your list_projects response.
|
|
2054
|
+
Proceeding anyway \u2014 verify the ID is correct.
|
|
2055
|
+
`
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
} catch {
|
|
2059
|
+
process.stderr.write(
|
|
2060
|
+
"Warning: could not verify project membership (offline or token issue). Proceeding anyway.\n"
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
await writeRepoBinding(root, projectId);
|
|
2065
|
+
const writtenPath = path6.join(root, "nolto.json");
|
|
2066
|
+
if (mode2 === "json") {
|
|
2067
|
+
printResult({ linked: true, projectId, projectBindingPath: writtenPath }, mode2);
|
|
2068
|
+
} else {
|
|
2069
|
+
process.stdout.write(
|
|
2070
|
+
`Linked this repo to project ${projectId} (wrote ${writtenPath}).
|
|
2071
|
+
Commit nolto.json to share the binding with your team.
|
|
2072
|
+
`
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
function register9(program, deps) {
|
|
2077
|
+
const cmd = program.command("link [projectId]").description(
|
|
2078
|
+
"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"
|
|
2079
|
+
).option("--show", "Show the current repo binding (path + projectId + source)").option("--unlink", "Remove the projectId key from nolto.json");
|
|
2080
|
+
cmd.action(async (projectId) => {
|
|
2081
|
+
const { output } = deps;
|
|
2082
|
+
const projectBindingPath = deps.projectBindingPath ?? null;
|
|
2083
|
+
const mode2 = output.mode;
|
|
2084
|
+
if (cmd.opts()["show"]) {
|
|
2085
|
+
await handleShow(deps, projectBindingPath, mode2);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
if (cmd.opts()["unlink"]) {
|
|
2089
|
+
if (projectBindingPath == null) {
|
|
2090
|
+
throw new CliError("No nolto.json found in this directory tree. Nothing to unlink.", 2);
|
|
2091
|
+
}
|
|
2092
|
+
await handleUnlink(projectBindingPath, mode2);
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
if (projectId == null || projectId.trim().length === 0) {
|
|
2096
|
+
throw new CliError(
|
|
2097
|
+
"Usage: nolto link <projectId> (provide a UUID)\nOr use --show to view the current binding, --unlink to remove it.",
|
|
2098
|
+
2
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
await performLink(deps, projectId, mode2);
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
|
|
1850
2105
|
// src/program.ts
|
|
1851
2106
|
function stripCommanderErrorPrefix(msg) {
|
|
1852
2107
|
return msg.startsWith("error: ") ? msg.slice("error: ".length) : msg;
|
|
@@ -1864,15 +2119,113 @@ function buildProgram(deps) {
|
|
|
1864
2119
|
register7(program, deps);
|
|
1865
2120
|
registerQueue(program, deps);
|
|
1866
2121
|
register8(program, deps);
|
|
2122
|
+
register9(program, deps);
|
|
1867
2123
|
return program;
|
|
1868
2124
|
}
|
|
1869
2125
|
|
|
2126
|
+
// src/update-notifier.ts
|
|
2127
|
+
import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
2128
|
+
import https from "https";
|
|
2129
|
+
import path7 from "path";
|
|
2130
|
+
var PACKAGE = "@nolto/cli";
|
|
2131
|
+
var CACHE_FILE = "update-check.json";
|
|
2132
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2133
|
+
var REQUEST_TIMEOUT_MS = 2e3;
|
|
2134
|
+
function isNewerVersion(latest, current) {
|
|
2135
|
+
const parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
2136
|
+
const a = parts(latest);
|
|
2137
|
+
const b = parts(current);
|
|
2138
|
+
for (let i = 0; i < 3; i++) {
|
|
2139
|
+
const x = a[i] ?? 0;
|
|
2140
|
+
const y = b[i] ?? 0;
|
|
2141
|
+
if (x !== y) return x > y;
|
|
2142
|
+
}
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
2145
|
+
function formatUpdateNotice(latest, current) {
|
|
2146
|
+
return `
|
|
2147
|
+
Update available: ${current} \u2192 ${latest} \xB7 npm i -g @nolto/cli@latest
|
|
2148
|
+
`;
|
|
2149
|
+
}
|
|
2150
|
+
function isDisabled(env) {
|
|
2151
|
+
return env["NO_UPDATE_NOTIFIER"] === "1" || env["NODE_ENV"] === "test" || Boolean(env["CI"]);
|
|
2152
|
+
}
|
|
2153
|
+
function fetchLatestFromRegistry() {
|
|
2154
|
+
return new Promise((resolve) => {
|
|
2155
|
+
const req = https.get(
|
|
2156
|
+
`https://registry.npmjs.org/${PACKAGE}/latest`,
|
|
2157
|
+
{ timeout: REQUEST_TIMEOUT_MS, headers: { accept: "application/json" } },
|
|
2158
|
+
(res) => {
|
|
2159
|
+
if (res.statusCode !== 200) {
|
|
2160
|
+
res.resume();
|
|
2161
|
+
resolve(null);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
let data = "";
|
|
2165
|
+
res.on("data", (chunk) => data += chunk);
|
|
2166
|
+
res.on("end", () => {
|
|
2167
|
+
try {
|
|
2168
|
+
const v = JSON.parse(data).version;
|
|
2169
|
+
resolve(typeof v === "string" ? v : null);
|
|
2170
|
+
} catch {
|
|
2171
|
+
resolve(null);
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
);
|
|
2176
|
+
req.on("socket", (s) => s.unref());
|
|
2177
|
+
req.on("timeout", () => {
|
|
2178
|
+
req.destroy();
|
|
2179
|
+
resolve(null);
|
|
2180
|
+
});
|
|
2181
|
+
req.on("error", () => resolve(null));
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
async function refreshCache(cachePath, now, fetchLatest) {
|
|
2185
|
+
const latest = await fetchLatest();
|
|
2186
|
+
if (!latest) return;
|
|
2187
|
+
await mkdir4(path7.dirname(cachePath), { recursive: true }).catch(() => void 0);
|
|
2188
|
+
const payload = { checkedAt: now, latest };
|
|
2189
|
+
await writeFile4(cachePath, JSON.stringify(payload), { mode: 384 }).catch(() => void 0);
|
|
2190
|
+
}
|
|
2191
|
+
async function checkForUpdate(opts) {
|
|
2192
|
+
if (isDisabled(opts.env)) return null;
|
|
2193
|
+
const cachePath = path7.join(opts.configDir, CACHE_FILE);
|
|
2194
|
+
let cache = {};
|
|
2195
|
+
try {
|
|
2196
|
+
cache = JSON.parse(await readFile5(cachePath, "utf8"));
|
|
2197
|
+
} catch {
|
|
2198
|
+
}
|
|
2199
|
+
if (typeof cache.checkedAt !== "number" || opts.now - cache.checkedAt > CACHE_TTL_MS) {
|
|
2200
|
+
void refreshCache(cachePath, opts.now, opts.fetchLatest ?? fetchLatestFromRegistry).catch(
|
|
2201
|
+
() => void 0
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
if (typeof cache.latest === "string" && isNewerVersion(cache.latest, opts.current)) {
|
|
2205
|
+
return cache.latest;
|
|
2206
|
+
}
|
|
2207
|
+
return null;
|
|
2208
|
+
}
|
|
2209
|
+
async function notifyUpdate(opts) {
|
|
2210
|
+
try {
|
|
2211
|
+
if (opts.isJson || !process.stderr.isTTY) return;
|
|
2212
|
+
const latest = await checkForUpdate({
|
|
2213
|
+
current: opts.current,
|
|
2214
|
+
configDir: getConfigDir(opts.env),
|
|
2215
|
+
env: opts.env,
|
|
2216
|
+
now: opts.now
|
|
2217
|
+
});
|
|
2218
|
+
if (latest) process.stderr.write(formatUpdateNotice(latest, opts.current));
|
|
2219
|
+
} catch {
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
1870
2223
|
// src/index.ts
|
|
1871
|
-
var __dirname2 =
|
|
2224
|
+
var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
|
|
1872
2225
|
var require2 = createRequire2(import.meta.url);
|
|
1873
2226
|
function getVersion() {
|
|
1874
2227
|
try {
|
|
1875
|
-
const pkgPath =
|
|
2228
|
+
const pkgPath = path8.resolve(__dirname2, "../package.json");
|
|
1876
2229
|
const pkg = require2(pkgPath);
|
|
1877
2230
|
return pkg.version ?? "0.0.0";
|
|
1878
2231
|
} catch {
|
|
@@ -1905,10 +2258,20 @@ async function main() {
|
|
|
1905
2258
|
process.exit();
|
|
1906
2259
|
}
|
|
1907
2260
|
}
|
|
2261
|
+
const projectBindingPath = findRepoBindingFile({ env: process.env, cwd: process.cwd() });
|
|
2262
|
+
const repoBinding = projectBindingPath != null ? await loadRepoBinding(projectBindingPath).catch((err) => {
|
|
2263
|
+
if (err instanceof CliError) {
|
|
2264
|
+
printError(err, mode);
|
|
2265
|
+
process.exitCode = err.exitCode;
|
|
2266
|
+
process.exit();
|
|
2267
|
+
}
|
|
2268
|
+
throw err;
|
|
2269
|
+
}) : null;
|
|
1908
2270
|
const settings = resolveSettings({
|
|
1909
2271
|
flags: { token: flagToken, baseUrl: flagBaseUrl, project: flagProject },
|
|
1910
2272
|
env: process.env,
|
|
1911
|
-
file: configFile
|
|
2273
|
+
file: configFile,
|
|
2274
|
+
repoBinding
|
|
1912
2275
|
});
|
|
1913
2276
|
const version = getVersion();
|
|
1914
2277
|
const caller = settings.token != null ? createMcpCaller({ baseUrl: settings.baseUrl, token: settings.token, version }) : {
|
|
@@ -1919,8 +2282,9 @@ async function main() {
|
|
|
1919
2282
|
);
|
|
1920
2283
|
}
|
|
1921
2284
|
};
|
|
1922
|
-
const program = buildProgram({ caller, settings, output: { mode }, version, configPath });
|
|
2285
|
+
const program = buildProgram({ caller, settings, output: { mode }, version, configPath, projectBindingPath });
|
|
1923
2286
|
await program.parseAsync(process.argv);
|
|
2287
|
+
await notifyUpdate({ current: version, env: process.env, isJson: mode === "json", now: Date.now() });
|
|
1924
2288
|
}
|
|
1925
2289
|
function extractFlag(argv, name) {
|
|
1926
2290
|
const idx = argv.indexOf(name);
|