@nolto/cli 0.1.0 → 0.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/dist/index.js +464 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createRequire } from "module";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
import
|
|
4
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
|
+
import path5 from "path";
|
|
7
7
|
import { CommanderError } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
@@ -77,6 +77,7 @@ var DOCUMENT_MAX_BYTES = 2 * 1024 * 1024;
|
|
|
77
77
|
var DOCUMENT_FILENAME_MAX = 255;
|
|
78
78
|
var DEFAULT_BASE_URL = "https://nolto.app";
|
|
79
79
|
var CLI_USER_AGENT_NAME = "nolto-cli";
|
|
80
|
+
var QUEUE_MAX_ENTRIES = 200;
|
|
80
81
|
|
|
81
82
|
// src/config.ts
|
|
82
83
|
var configSchema = z.object({
|
|
@@ -270,6 +271,30 @@ import { Command } from "commander";
|
|
|
270
271
|
|
|
271
272
|
// src/commands/init.ts
|
|
272
273
|
import readline from "readline/promises";
|
|
274
|
+
import { createRequire } from "module";
|
|
275
|
+
import { fileURLToPath } from "url";
|
|
276
|
+
import path2 from "path";
|
|
277
|
+
import fs from "fs";
|
|
278
|
+
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
279
|
+
var _require = createRequire(import.meta.url);
|
|
280
|
+
function getCliVersion() {
|
|
281
|
+
const candidates = [
|
|
282
|
+
path2.resolve(__dirname, "../package.json"),
|
|
283
|
+
// bundled: dist/../package.json
|
|
284
|
+
path2.resolve(__dirname, "../../package.json")
|
|
285
|
+
// source: src/commands/../../package.json
|
|
286
|
+
];
|
|
287
|
+
for (const pkgPath of candidates) {
|
|
288
|
+
if (fs.existsSync(pkgPath)) {
|
|
289
|
+
try {
|
|
290
|
+
const pkg = _require(pkgPath);
|
|
291
|
+
return pkg.version ?? "0.0.0";
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return "0.0.0";
|
|
297
|
+
}
|
|
273
298
|
function register(program, _deps) {
|
|
274
299
|
program.command("init").description("Interactive setup: configure token, base URL, and default project.").option("--force", "Overwrite existing config without prompting").action(async (opts) => {
|
|
275
300
|
const configPath = getConfigPath(process.env);
|
|
@@ -301,7 +326,7 @@ function register(program, _deps) {
|
|
|
301
326
|
if (token.length === 0) {
|
|
302
327
|
throw new CliError("Token is required.", 2);
|
|
303
328
|
}
|
|
304
|
-
const caller = createMcpCaller({ baseUrl, token, version:
|
|
329
|
+
const caller = createMcpCaller({ baseUrl, token, version: getCliVersion() });
|
|
305
330
|
let projects = [];
|
|
306
331
|
try {
|
|
307
332
|
const result = await caller.call("list_projects", {});
|
|
@@ -552,9 +577,9 @@ import { z as z2 } from "zod";
|
|
|
552
577
|
|
|
553
578
|
// src/fsx.ts
|
|
554
579
|
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
555
|
-
import
|
|
580
|
+
import path3 from "path";
|
|
556
581
|
async function readPlanFile(filePath) {
|
|
557
|
-
const absPath =
|
|
582
|
+
const absPath = path3.resolve(filePath);
|
|
558
583
|
let content;
|
|
559
584
|
try {
|
|
560
585
|
content = await readFile2(absPath, "utf8");
|
|
@@ -575,11 +600,11 @@ async function readPlanFile(filePath) {
|
|
|
575
600
|
2
|
|
576
601
|
);
|
|
577
602
|
}
|
|
578
|
-
const stem =
|
|
603
|
+
const stem = path3.basename(absPath, path3.extname(absPath));
|
|
579
604
|
return { content: trimmed, sourcePath: absPath, titleFallback: stem };
|
|
580
605
|
}
|
|
581
606
|
async function readDocFile(filePath) {
|
|
582
|
-
const absPath =
|
|
607
|
+
const absPath = path3.resolve(filePath);
|
|
583
608
|
let fileSize;
|
|
584
609
|
try {
|
|
585
610
|
const info = await stat2(absPath);
|
|
@@ -601,7 +626,7 @@ async function readDocFile(filePath) {
|
|
|
601
626
|
throw new CliError(`Cannot read file: ${absPath}: ${String(err)}`, 2);
|
|
602
627
|
}
|
|
603
628
|
const isBinary = buf.includes(0) || !isUtf8RoundTrip(buf);
|
|
604
|
-
const rawFilename =
|
|
629
|
+
const rawFilename = path3.basename(absPath);
|
|
605
630
|
const filename = rawFilename.slice(0, DOCUMENT_FILENAME_MAX);
|
|
606
631
|
if (isBinary) {
|
|
607
632
|
return { content: buf.toString("base64"), encoding: "base64", filename };
|
|
@@ -732,8 +757,13 @@ function registerPlanRegisterSubcommand(plan, deps) {
|
|
|
732
757
|
}
|
|
733
758
|
const phaseResult = phaseArraySchema.safeParse(parsedPhases);
|
|
734
759
|
if (!phaseResult.success) {
|
|
735
|
-
const
|
|
736
|
-
|
|
760
|
+
const issue = phaseResult.error.issues[0];
|
|
761
|
+
if (issue?.code === "too_big" && issue.path.length === 0) {
|
|
762
|
+
throw new CliError(`Too many phases (max ${PHASES_MAX}).`, 2);
|
|
763
|
+
}
|
|
764
|
+
const fieldPath = issue?.path.join(".") ?? "";
|
|
765
|
+
const fieldDesc = fieldPath.length > 0 ? `field "${fieldPath}"` : "unknown field";
|
|
766
|
+
throw new CliError(`Invalid --phases JSON: ${fieldDesc}`, 2);
|
|
737
767
|
}
|
|
738
768
|
parsedPhases = phaseResult.data;
|
|
739
769
|
}
|
|
@@ -925,7 +955,7 @@ function register5(program, deps) {
|
|
|
925
955
|
}
|
|
926
956
|
let round;
|
|
927
957
|
if (opts.round != null) {
|
|
928
|
-
if (
|
|
958
|
+
if (!/^[1-9]\d*$/.test(opts.round)) {
|
|
929
959
|
throw new CliError(`--round must be a positive integer, got "${opts.round}"`, 2);
|
|
930
960
|
}
|
|
931
961
|
const parsed = Number(opts.round);
|
|
@@ -994,27 +1024,440 @@ function register6(program, deps) {
|
|
|
994
1024
|
});
|
|
995
1025
|
}
|
|
996
1026
|
|
|
1027
|
+
// src/queue-file.ts
|
|
1028
|
+
import { openSync, closeSync, writeFileSync, unlinkSync, renameSync, statSync } from "fs";
|
|
1029
|
+
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, unlink, appendFile } from "fs/promises";
|
|
1030
|
+
import path4 from "path";
|
|
1031
|
+
import crypto from "crypto";
|
|
1032
|
+
function queueFilePath(projectDir) {
|
|
1033
|
+
return path4.join(projectDir, ".nolto", "pending.jsonl");
|
|
1034
|
+
}
|
|
1035
|
+
function lockFilePath(projectDir) {
|
|
1036
|
+
return path4.join(projectDir, ".nolto", "flush.lock");
|
|
1037
|
+
}
|
|
1038
|
+
function logFilePath(projectDir) {
|
|
1039
|
+
return path4.join(projectDir, ".nolto", "flush.log");
|
|
1040
|
+
}
|
|
1041
|
+
function resolveQueueDir(inputs) {
|
|
1042
|
+
if (inputs.flagDir != null) return inputs.flagDir;
|
|
1043
|
+
if (inputs.env["NOLTO_QUEUE_DIR"]) return inputs.env["NOLTO_QUEUE_DIR"];
|
|
1044
|
+
if (inputs.env["CLAUDE_PROJECT_DIR"]) return inputs.env["CLAUDE_PROJECT_DIR"];
|
|
1045
|
+
return findAncestorWithMarker(inputs.cwd);
|
|
1046
|
+
}
|
|
1047
|
+
function findAncestorWithMarker(startDir) {
|
|
1048
|
+
let current = startDir;
|
|
1049
|
+
while (true) {
|
|
1050
|
+
if (hasMarkerSync(current, ".git") || hasMarkerSync(current, ".nolto")) {
|
|
1051
|
+
return current;
|
|
1052
|
+
}
|
|
1053
|
+
const parent = path4.dirname(current);
|
|
1054
|
+
if (parent === current) break;
|
|
1055
|
+
current = parent;
|
|
1056
|
+
}
|
|
1057
|
+
return startDir;
|
|
1058
|
+
}
|
|
1059
|
+
function hasMarkerSync(dir, marker) {
|
|
1060
|
+
try {
|
|
1061
|
+
statSync(path4.join(dir, marker));
|
|
1062
|
+
return true;
|
|
1063
|
+
} catch {
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
async function readQueue(projectDir) {
|
|
1068
|
+
const filePath = queueFilePath(projectDir);
|
|
1069
|
+
let raw;
|
|
1070
|
+
try {
|
|
1071
|
+
raw = await readFile3(filePath, "utf8");
|
|
1072
|
+
} catch {
|
|
1073
|
+
return [];
|
|
1074
|
+
}
|
|
1075
|
+
const entries = [];
|
|
1076
|
+
for (const line of raw.split("\n")) {
|
|
1077
|
+
const trimmed = line.trim();
|
|
1078
|
+
if (!trimmed) continue;
|
|
1079
|
+
try {
|
|
1080
|
+
const obj = JSON.parse(trimmed);
|
|
1081
|
+
entries.push(obj);
|
|
1082
|
+
} catch {
|
|
1083
|
+
await appendLog(projectDir, "warn", `malformed queue line skipped: ${trimmed.slice(0, 80)}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return entries;
|
|
1087
|
+
}
|
|
1088
|
+
async function appendEntry(projectDir, entry) {
|
|
1089
|
+
const noltoDir = path4.join(projectDir, ".nolto");
|
|
1090
|
+
await mkdir2(noltoDir, { recursive: true, mode: 448 });
|
|
1091
|
+
const existing = await readQueue(projectDir);
|
|
1092
|
+
if (existing.length >= QUEUE_MAX_ENTRIES) {
|
|
1093
|
+
throw new CliError(
|
|
1094
|
+
`Queue is full (${QUEUE_MAX_ENTRIES} entries). Flush before adding more.`,
|
|
1095
|
+
2
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
const newEntry = {
|
|
1099
|
+
id: crypto.randomUUID(),
|
|
1100
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1101
|
+
tool: entry.tool,
|
|
1102
|
+
args: { ...entry.args }
|
|
1103
|
+
};
|
|
1104
|
+
const line = JSON.stringify(newEntry) + "\n";
|
|
1105
|
+
try {
|
|
1106
|
+
await appendFile(queueFilePath(projectDir), line, "utf8");
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
if (err instanceof CliError) throw err;
|
|
1109
|
+
throw new CliError(
|
|
1110
|
+
`Failed to write queue entry: ${err.message ?? String(err)}`,
|
|
1111
|
+
2
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
return newEntry;
|
|
1115
|
+
}
|
|
1116
|
+
async function atomicRewriteQueue(projectDir, entries) {
|
|
1117
|
+
const filePath = queueFilePath(projectDir);
|
|
1118
|
+
if (entries.length === 0) {
|
|
1119
|
+
try {
|
|
1120
|
+
await unlink(filePath);
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
const code = err.code;
|
|
1123
|
+
if (code !== "ENOENT") throw err;
|
|
1124
|
+
}
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const noltoDir = path4.join(projectDir, ".nolto");
|
|
1128
|
+
await mkdir2(noltoDir, { recursive: true });
|
|
1129
|
+
const tmpPath = filePath + ".tmp";
|
|
1130
|
+
const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
1131
|
+
await writeFile2(tmpPath, content, "utf8");
|
|
1132
|
+
renameSync(tmpPath, filePath);
|
|
1133
|
+
}
|
|
1134
|
+
async function acquireLock(projectDir) {
|
|
1135
|
+
const noltoDir = path4.join(projectDir, ".nolto");
|
|
1136
|
+
await mkdir2(noltoDir, { recursive: true });
|
|
1137
|
+
const lockPath = lockFilePath(projectDir);
|
|
1138
|
+
return tryAcquire(lockPath);
|
|
1139
|
+
}
|
|
1140
|
+
async function tryAcquire(lockPath) {
|
|
1141
|
+
try {
|
|
1142
|
+
const fd = openSync(lockPath, "wx");
|
|
1143
|
+
writeFileSync(fd, String(process.pid));
|
|
1144
|
+
closeSync(fd);
|
|
1145
|
+
return makeLockHandle(lockPath);
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
if (err.code !== "EEXIST") throw err;
|
|
1148
|
+
}
|
|
1149
|
+
let pidStr;
|
|
1150
|
+
try {
|
|
1151
|
+
pidStr = await readFile3(lockPath, "utf8");
|
|
1152
|
+
} catch {
|
|
1153
|
+
try {
|
|
1154
|
+
unlinkSync(lockPath);
|
|
1155
|
+
} catch {
|
|
1156
|
+
}
|
|
1157
|
+
return tryAcquire(lockPath);
|
|
1158
|
+
}
|
|
1159
|
+
const pid = parseInt(pidStr, 10);
|
|
1160
|
+
if (isNaN(pid)) {
|
|
1161
|
+
try {
|
|
1162
|
+
unlinkSync(lockPath);
|
|
1163
|
+
} catch {
|
|
1164
|
+
}
|
|
1165
|
+
return tryAcquire(lockPath);
|
|
1166
|
+
}
|
|
1167
|
+
try {
|
|
1168
|
+
process.kill(pid, 0);
|
|
1169
|
+
return null;
|
|
1170
|
+
} catch (sigErr) {
|
|
1171
|
+
if (sigErr.code === "ESRCH") {
|
|
1172
|
+
try {
|
|
1173
|
+
unlinkSync(lockPath);
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
return tryAcquire(lockPath);
|
|
1177
|
+
}
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function makeLockHandle(lockPath) {
|
|
1182
|
+
let released = false;
|
|
1183
|
+
return {
|
|
1184
|
+
async release() {
|
|
1185
|
+
if (released) return;
|
|
1186
|
+
released = true;
|
|
1187
|
+
try {
|
|
1188
|
+
unlinkSync(lockPath);
|
|
1189
|
+
} catch {
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
async function appendLog(projectDir, level, message) {
|
|
1195
|
+
try {
|
|
1196
|
+
const noltoDir = path4.join(projectDir, ".nolto");
|
|
1197
|
+
await mkdir2(noltoDir, { recursive: true });
|
|
1198
|
+
const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}
|
|
1199
|
+
`;
|
|
1200
|
+
await appendFile(logFilePath(projectDir), line, "utf8");
|
|
1201
|
+
} catch {
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/commands/queue.ts
|
|
1206
|
+
function withQueueDir(cmd) {
|
|
1207
|
+
return cmd.option("--queue-dir <path>", "Override project directory for queue files");
|
|
1208
|
+
}
|
|
1209
|
+
function registerQueue(program, deps) {
|
|
1210
|
+
const queue = program.command("queue").description("Queue a progress report for later flush.");
|
|
1211
|
+
withQueueDir(
|
|
1212
|
+
queue.command("phase-status <planId> <phaseId> <status>").description("Queue a phase status update (offline, no token required).").option("--message <text>", "Optional message")
|
|
1213
|
+
).action(
|
|
1214
|
+
async (planId, phaseId, status, opts) => {
|
|
1215
|
+
if (!PLAN_STATUSES.includes(status)) {
|
|
1216
|
+
throw new CliError(
|
|
1217
|
+
`Invalid status "${status}". Valid values: ${PLAN_STATUSES.join(", ")}`,
|
|
1218
|
+
2
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
const projectDir = resolveQueueDir({
|
|
1222
|
+
flagDir: opts.queueDir,
|
|
1223
|
+
env: process.env,
|
|
1224
|
+
cwd: process.cwd()
|
|
1225
|
+
});
|
|
1226
|
+
const args = {
|
|
1227
|
+
planId,
|
|
1228
|
+
phaseId,
|
|
1229
|
+
status
|
|
1230
|
+
};
|
|
1231
|
+
if (deps.settings.defaultProjectId != null) args["projectId"] = deps.settings.defaultProjectId;
|
|
1232
|
+
if (opts.message != null) args["message"] = opts.message;
|
|
1233
|
+
const entry = await appendEntry(projectDir, { tool: "update_phase_status", args });
|
|
1234
|
+
if (deps.output.mode === "json") {
|
|
1235
|
+
printResult({ queued: true, id: entry.id }, "json");
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
process.stdout.write(`Queued update_phase_status.
|
|
1239
|
+
`);
|
|
1240
|
+
}
|
|
1241
|
+
);
|
|
1242
|
+
withQueueDir(
|
|
1243
|
+
queue.command("phase-test <planId> <phaseId> <verdict>").description("Queue a phase test result (offline, no token required).").option("--round <n>", "Test round number (positive integer)").option("--summary <text>", "Test summary")
|
|
1244
|
+
).action(
|
|
1245
|
+
async (planId, phaseId, verdict, opts) => {
|
|
1246
|
+
if (!TEST_VERDICTS.includes(verdict)) {
|
|
1247
|
+
throw new CliError(
|
|
1248
|
+
`Invalid verdict "${verdict}". Valid values: ${TEST_VERDICTS.join(", ")}`,
|
|
1249
|
+
2
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
let round;
|
|
1253
|
+
if (opts.round != null) {
|
|
1254
|
+
if (!/^[1-9]\d*$/.test(opts.round)) {
|
|
1255
|
+
throw new CliError(`--round must be a positive integer, got "${opts.round}"`, 2);
|
|
1256
|
+
}
|
|
1257
|
+
const parsed = Number(opts.round);
|
|
1258
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
1259
|
+
throw new CliError(`--round must be a positive integer, got "${opts.round}"`, 2);
|
|
1260
|
+
}
|
|
1261
|
+
round = parsed;
|
|
1262
|
+
}
|
|
1263
|
+
const projectDir = resolveQueueDir({
|
|
1264
|
+
flagDir: opts.queueDir,
|
|
1265
|
+
env: process.env,
|
|
1266
|
+
cwd: process.cwd()
|
|
1267
|
+
});
|
|
1268
|
+
const args = {
|
|
1269
|
+
planId,
|
|
1270
|
+
phaseId,
|
|
1271
|
+
verdict
|
|
1272
|
+
};
|
|
1273
|
+
if (deps.settings.defaultProjectId != null) args["projectId"] = deps.settings.defaultProjectId;
|
|
1274
|
+
if (round != null) args["round"] = round;
|
|
1275
|
+
if (opts.summary != null) args["summary"] = opts.summary;
|
|
1276
|
+
const entry = await appendEntry(projectDir, { tool: "record_phase_test_result", args });
|
|
1277
|
+
if (deps.output.mode === "json") {
|
|
1278
|
+
printResult({ queued: true, id: entry.id }, "json");
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
process.stdout.write(`Queued record_phase_test_result.
|
|
1282
|
+
`);
|
|
1283
|
+
}
|
|
1284
|
+
);
|
|
1285
|
+
withQueueDir(
|
|
1286
|
+
queue.command("plan-status <planId> <status>").description("Queue a plan status update (offline, no token required).").option("--message <text>", "Optional message")
|
|
1287
|
+
).action(
|
|
1288
|
+
async (planId, status, opts) => {
|
|
1289
|
+
if (!PLAN_STATUSES.includes(status)) {
|
|
1290
|
+
throw new CliError(
|
|
1291
|
+
`Invalid status "${status}". Valid values: ${PLAN_STATUSES.join(", ")}`,
|
|
1292
|
+
2
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
const projectDir = resolveQueueDir({
|
|
1296
|
+
flagDir: opts.queueDir,
|
|
1297
|
+
env: process.env,
|
|
1298
|
+
cwd: process.cwd()
|
|
1299
|
+
});
|
|
1300
|
+
const args = {
|
|
1301
|
+
planId,
|
|
1302
|
+
status
|
|
1303
|
+
};
|
|
1304
|
+
if (deps.settings.defaultProjectId != null) args["projectId"] = deps.settings.defaultProjectId;
|
|
1305
|
+
if (opts.message != null) args["message"] = opts.message;
|
|
1306
|
+
const entry = await appendEntry(projectDir, { tool: "update_plan_status", args });
|
|
1307
|
+
if (deps.output.mode === "json") {
|
|
1308
|
+
printResult({ queued: true, id: entry.id }, "json");
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
process.stdout.write(`Queued update_plan_status.
|
|
1312
|
+
`);
|
|
1313
|
+
}
|
|
1314
|
+
);
|
|
1315
|
+
withQueueDir(
|
|
1316
|
+
queue.command("plan-review <planId> <verdict>").description("Queue a plan review (offline, no token required).").option("--summary <text>", "Review summary")
|
|
1317
|
+
).action(
|
|
1318
|
+
async (planId, verdict, opts) => {
|
|
1319
|
+
if (!REVIEW_VERDICTS.includes(verdict)) {
|
|
1320
|
+
throw new CliError(
|
|
1321
|
+
`Invalid verdict "${verdict}". Valid values: ${REVIEW_VERDICTS.join(", ")}`,
|
|
1322
|
+
2
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
const projectDir = resolveQueueDir({
|
|
1326
|
+
flagDir: opts.queueDir,
|
|
1327
|
+
env: process.env,
|
|
1328
|
+
cwd: process.cwd()
|
|
1329
|
+
});
|
|
1330
|
+
const args = {
|
|
1331
|
+
planId,
|
|
1332
|
+
verdict
|
|
1333
|
+
};
|
|
1334
|
+
if (deps.settings.defaultProjectId != null) args["projectId"] = deps.settings.defaultProjectId;
|
|
1335
|
+
if (opts.summary != null) args["summary"] = opts.summary;
|
|
1336
|
+
const entry = await appendEntry(projectDir, { tool: "record_plan_review", args });
|
|
1337
|
+
if (deps.output.mode === "json") {
|
|
1338
|
+
printResult({ queued: true, id: entry.id }, "json");
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
process.stdout.write(`Queued record_plan_review.
|
|
1342
|
+
`);
|
|
1343
|
+
}
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/detach.ts
|
|
1348
|
+
import { spawn } from "child_process";
|
|
1349
|
+
function spawnDetachedFlush(scriptPath, queueDir) {
|
|
1350
|
+
const extraArgs = queueDir != null ? ["--queue-dir", queueDir] : [];
|
|
1351
|
+
const child = spawn(
|
|
1352
|
+
process.execPath,
|
|
1353
|
+
[scriptPath, "flush", ...extraArgs],
|
|
1354
|
+
{
|
|
1355
|
+
detached: true,
|
|
1356
|
+
stdio: "ignore",
|
|
1357
|
+
env: process.env
|
|
1358
|
+
}
|
|
1359
|
+
);
|
|
1360
|
+
child.unref();
|
|
1361
|
+
return child.pid;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// src/commands/flush.ts
|
|
1365
|
+
async function runFlushWorker(deps, projectDir) {
|
|
1366
|
+
const lock = await acquireLock(projectDir);
|
|
1367
|
+
if (lock === null) {
|
|
1368
|
+
process.stdout.write("Another flush is running.\n");
|
|
1369
|
+
return { flushed: 0, remaining: 0, failed: 0 };
|
|
1370
|
+
}
|
|
1371
|
+
try {
|
|
1372
|
+
const entries = await readQueue(projectDir);
|
|
1373
|
+
if (entries.length === 0) {
|
|
1374
|
+
process.stdout.write("Nothing to flush.\n");
|
|
1375
|
+
return { flushed: 0, remaining: 0, failed: 0 };
|
|
1376
|
+
}
|
|
1377
|
+
if (deps.settings.token == null) {
|
|
1378
|
+
await appendLog(projectDir, "warn", "flush skipped: no token configured");
|
|
1379
|
+
return { flushed: 0, remaining: entries.length, failed: 0 };
|
|
1380
|
+
}
|
|
1381
|
+
let flushed = 0;
|
|
1382
|
+
let remaining = entries.slice();
|
|
1383
|
+
for (const entry of entries) {
|
|
1384
|
+
try {
|
|
1385
|
+
await deps.caller.call(entry.tool, entry.args);
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
const cliErr = err instanceof CliError ? err : new CliError(String(err), 5);
|
|
1388
|
+
await appendLog(
|
|
1389
|
+
projectDir,
|
|
1390
|
+
"error",
|
|
1391
|
+
`${entry.tool} id=${entry.id} exit=${cliErr.exitCode} ${cliErr.message}`
|
|
1392
|
+
);
|
|
1393
|
+
return { flushed, remaining: remaining.length, failed: 1 };
|
|
1394
|
+
}
|
|
1395
|
+
flushed++;
|
|
1396
|
+
remaining = remaining.slice(1);
|
|
1397
|
+
await atomicRewriteQueue(projectDir, remaining);
|
|
1398
|
+
}
|
|
1399
|
+
return { flushed, remaining: 0, failed: 0 };
|
|
1400
|
+
} finally {
|
|
1401
|
+
await lock.release();
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function register7(program, deps) {
|
|
1405
|
+
program.command("flush").description("Flush the pending queue, sending each entry to the Nolto MCP server.").option("--detach", "Spawn a detached background worker and exit immediately.").option("--queue-dir <path>", "Override project directory for queue files").action(async (opts) => {
|
|
1406
|
+
const projectDir = resolveQueueDir({
|
|
1407
|
+
flagDir: opts.queueDir,
|
|
1408
|
+
env: process.env,
|
|
1409
|
+
cwd: process.cwd()
|
|
1410
|
+
});
|
|
1411
|
+
if (opts.detach) {
|
|
1412
|
+
spawnDetachedFlush(process.argv[1], opts.queueDir);
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
try {
|
|
1416
|
+
const summary = await runFlushWorker(deps, projectDir);
|
|
1417
|
+
if (deps.output.mode === "json") {
|
|
1418
|
+
process.stdout.write(JSON.stringify(summary) + "\n");
|
|
1419
|
+
} else {
|
|
1420
|
+
if (summary.flushed > 0 || summary.failed > 0) {
|
|
1421
|
+
process.stdout.write(
|
|
1422
|
+
`Flushed ${summary.flushed}, remaining ${summary.remaining}, failed ${summary.failed}.
|
|
1423
|
+
`
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1429
|
+
await appendLog(projectDir, "error", `unexpected flush error: ${msg}`);
|
|
1430
|
+
process.stderr.write(`Flush error (logged to .nolto/flush.log): ${msg}
|
|
1431
|
+
`);
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
|
|
997
1436
|
// src/program.ts
|
|
1437
|
+
function stripCommanderErrorPrefix(msg) {
|
|
1438
|
+
return msg.startsWith("error: ") ? msg.slice("error: ".length) : msg;
|
|
1439
|
+
}
|
|
998
1440
|
function buildProgram(deps) {
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
} :
|
|
1002
|
-
const program = new Command("nolto").version(deps.version, "-V, --version", "Print version number").exitOverride().configureOutput(writeErr != null ? { writeErr } : {}).description("Nolto CLI \u2014 register plans and update progress from your terminal.").option("--token <value>", "API token (overrides env/file)").option("--base-url <url>", "Nolto base URL (default: https://nolto.app)").option("--project <projectId>", "Default project ID").option("--json", "Output as JSON");
|
|
1441
|
+
const writeErr = (_msg) => {
|
|
1442
|
+
};
|
|
1443
|
+
const program = new Command("nolto").version(deps.version, "-V, --version", "Print version number").exitOverride().configureOutput({ writeErr }).description("Nolto CLI \u2014 register plans and update progress from your terminal.").option("--token <value>", "API token (overrides env/file)").option("--base-url <url>", "Nolto base URL (default: https://nolto.app)").option("--project <projectId>", "Default project ID").option("--json", "Output as JSON");
|
|
1003
1444
|
register(program, deps);
|
|
1004
1445
|
register2(program, deps);
|
|
1005
1446
|
register3(program, deps);
|
|
1006
1447
|
register4(program, deps);
|
|
1007
1448
|
register5(program, deps);
|
|
1008
1449
|
register6(program, deps);
|
|
1450
|
+
registerQueue(program, deps);
|
|
1451
|
+
register7(program, deps);
|
|
1009
1452
|
return program;
|
|
1010
1453
|
}
|
|
1011
1454
|
|
|
1012
1455
|
// src/index.ts
|
|
1013
|
-
var
|
|
1014
|
-
var require2 =
|
|
1456
|
+
var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
|
|
1457
|
+
var require2 = createRequire2(import.meta.url);
|
|
1015
1458
|
function getVersion() {
|
|
1016
1459
|
try {
|
|
1017
|
-
const pkgPath =
|
|
1460
|
+
const pkgPath = path5.resolve(__dirname2, "../package.json");
|
|
1018
1461
|
const pkg = require2(pkgPath);
|
|
1019
1462
|
return pkg.version ?? "0.0.0";
|
|
1020
1463
|
} catch {
|
|
@@ -1076,7 +1519,8 @@ main().catch((err) => {
|
|
|
1076
1519
|
if (err.exitCode === 0) {
|
|
1077
1520
|
process.exit(0);
|
|
1078
1521
|
}
|
|
1079
|
-
const
|
|
1522
|
+
const cleanMsg = stripCommanderErrorPrefix(err.message);
|
|
1523
|
+
const cliErr2 = new CliError(cleanMsg, 2);
|
|
1080
1524
|
printError(cliErr2, mode);
|
|
1081
1525
|
process.exitCode = 2;
|
|
1082
1526
|
return;
|