@nolto/cli 0.1.1 → 0.2.1
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 +438 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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 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({
|
|
@@ -274,6 +275,22 @@ import { createRequire } from "module";
|
|
|
274
275
|
import { fileURLToPath } from "url";
|
|
275
276
|
import path2 from "path";
|
|
276
277
|
import fs from "fs";
|
|
278
|
+
|
|
279
|
+
// src/unwrap.ts
|
|
280
|
+
function unwrapList(result, key) {
|
|
281
|
+
if (Array.isArray(result)) {
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
if (result != null && typeof result === "object") {
|
|
285
|
+
const inner = result[key];
|
|
286
|
+
if (Array.isArray(inner)) {
|
|
287
|
+
return inner;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/commands/init.ts
|
|
277
294
|
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
278
295
|
var _require = createRequire(import.meta.url);
|
|
279
296
|
function getCliVersion() {
|
|
@@ -329,9 +346,7 @@ function register(program, _deps) {
|
|
|
329
346
|
let projects = [];
|
|
330
347
|
try {
|
|
331
348
|
const result = await caller.call("list_projects", {});
|
|
332
|
-
|
|
333
|
-
projects = result;
|
|
334
|
-
}
|
|
349
|
+
projects = unwrapList(result, "projects");
|
|
335
350
|
} catch (err) {
|
|
336
351
|
if (err instanceof CliError && err.exitCode === 3) {
|
|
337
352
|
throw new CliError(`Token rejected by ${baseUrl}`, 3, "Check that your token is valid and has not been revoked.");
|
|
@@ -473,9 +488,7 @@ function register2(program, deps) {
|
|
|
473
488
|
if (settings.token != null) {
|
|
474
489
|
try {
|
|
475
490
|
const result = await deps.caller.call("list_projects", {});
|
|
476
|
-
|
|
477
|
-
projectCount = result.length;
|
|
478
|
-
}
|
|
491
|
+
projectCount = unwrapList(result, "projects").length;
|
|
479
492
|
} catch {
|
|
480
493
|
}
|
|
481
494
|
}
|
|
@@ -500,7 +513,7 @@ function register2(program, deps) {
|
|
|
500
513
|
["configPath", configPath],
|
|
501
514
|
["token", tokenDisplay],
|
|
502
515
|
["defaultProject", `${settings.defaultProjectId ?? "none"} (source: ${settings.source.project})`],
|
|
503
|
-
["projects", projectCount != null ? String(projectCount) : "(no token)"]
|
|
516
|
+
["projects", projectCount != null ? String(projectCount) : settings.token != null ? "(unavailable)" : "(no token)"]
|
|
504
517
|
];
|
|
505
518
|
const maxKey = lines.reduce((m, [k]) => Math.max(m, k.length), 0);
|
|
506
519
|
for (const [key, val] of lines) {
|
|
@@ -529,7 +542,7 @@ function register3(program, deps) {
|
|
|
529
542
|
printResult(result, "json");
|
|
530
543
|
return;
|
|
531
544
|
}
|
|
532
|
-
const rows =
|
|
545
|
+
const rows = unwrapList(result, "projects");
|
|
533
546
|
const defaultId = deps.settings.defaultProjectId;
|
|
534
547
|
const tableRows = rows.map((p) => ({
|
|
535
548
|
id: p.id ?? "",
|
|
@@ -847,12 +860,12 @@ function register4(program, deps) {
|
|
|
847
860
|
printResult(result, "json");
|
|
848
861
|
return;
|
|
849
862
|
}
|
|
850
|
-
const rows =
|
|
863
|
+
const rows = unwrapList(result, "plans");
|
|
851
864
|
const tableRows = rows.map((p) => ({
|
|
852
865
|
id: p.id ?? "",
|
|
853
|
-
title: p.title ?? "",
|
|
866
|
+
title: p.display_title ?? p.raw_title ?? p.title ?? "",
|
|
854
867
|
status: p.status ?? "",
|
|
855
|
-
createdAt: p.createdAt ?? ""
|
|
868
|
+
createdAt: p.created_at ?? p.createdAt ?? ""
|
|
856
869
|
}));
|
|
857
870
|
process.stdout.write(formatTable(tableRows, ["id", "title", "status", "createdAt"]) + "\n");
|
|
858
871
|
});
|
|
@@ -1023,6 +1036,415 @@ function register6(program, deps) {
|
|
|
1023
1036
|
});
|
|
1024
1037
|
}
|
|
1025
1038
|
|
|
1039
|
+
// src/queue-file.ts
|
|
1040
|
+
import { openSync, closeSync, writeFileSync, unlinkSync, renameSync, statSync } from "fs";
|
|
1041
|
+
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, unlink, appendFile } from "fs/promises";
|
|
1042
|
+
import path4 from "path";
|
|
1043
|
+
import crypto from "crypto";
|
|
1044
|
+
function queueFilePath(projectDir) {
|
|
1045
|
+
return path4.join(projectDir, ".nolto", "pending.jsonl");
|
|
1046
|
+
}
|
|
1047
|
+
function lockFilePath(projectDir) {
|
|
1048
|
+
return path4.join(projectDir, ".nolto", "flush.lock");
|
|
1049
|
+
}
|
|
1050
|
+
function logFilePath(projectDir) {
|
|
1051
|
+
return path4.join(projectDir, ".nolto", "flush.log");
|
|
1052
|
+
}
|
|
1053
|
+
function resolveQueueDir(inputs) {
|
|
1054
|
+
if (inputs.flagDir != null) return inputs.flagDir;
|
|
1055
|
+
if (inputs.env["NOLTO_QUEUE_DIR"]) return inputs.env["NOLTO_QUEUE_DIR"];
|
|
1056
|
+
if (inputs.env["CLAUDE_PROJECT_DIR"]) return inputs.env["CLAUDE_PROJECT_DIR"];
|
|
1057
|
+
return findAncestorWithMarker(inputs.cwd);
|
|
1058
|
+
}
|
|
1059
|
+
function findAncestorWithMarker(startDir) {
|
|
1060
|
+
let current = startDir;
|
|
1061
|
+
while (true) {
|
|
1062
|
+
if (hasMarkerSync(current, ".git") || hasMarkerSync(current, ".nolto")) {
|
|
1063
|
+
return current;
|
|
1064
|
+
}
|
|
1065
|
+
const parent = path4.dirname(current);
|
|
1066
|
+
if (parent === current) break;
|
|
1067
|
+
current = parent;
|
|
1068
|
+
}
|
|
1069
|
+
return startDir;
|
|
1070
|
+
}
|
|
1071
|
+
function hasMarkerSync(dir, marker) {
|
|
1072
|
+
try {
|
|
1073
|
+
statSync(path4.join(dir, marker));
|
|
1074
|
+
return true;
|
|
1075
|
+
} catch {
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async function readQueue(projectDir) {
|
|
1080
|
+
const filePath = queueFilePath(projectDir);
|
|
1081
|
+
let raw;
|
|
1082
|
+
try {
|
|
1083
|
+
raw = await readFile3(filePath, "utf8");
|
|
1084
|
+
} catch {
|
|
1085
|
+
return [];
|
|
1086
|
+
}
|
|
1087
|
+
const entries = [];
|
|
1088
|
+
for (const line of raw.split("\n")) {
|
|
1089
|
+
const trimmed = line.trim();
|
|
1090
|
+
if (!trimmed) continue;
|
|
1091
|
+
try {
|
|
1092
|
+
const obj = JSON.parse(trimmed);
|
|
1093
|
+
entries.push(obj);
|
|
1094
|
+
} catch {
|
|
1095
|
+
await appendLog(projectDir, "warn", `malformed queue line skipped: ${trimmed.slice(0, 80)}`);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return entries;
|
|
1099
|
+
}
|
|
1100
|
+
async function appendEntry(projectDir, entry) {
|
|
1101
|
+
const noltoDir = path4.join(projectDir, ".nolto");
|
|
1102
|
+
await mkdir2(noltoDir, { recursive: true, mode: 448 });
|
|
1103
|
+
const existing = await readQueue(projectDir);
|
|
1104
|
+
if (existing.length >= QUEUE_MAX_ENTRIES) {
|
|
1105
|
+
throw new CliError(
|
|
1106
|
+
`Queue is full (${QUEUE_MAX_ENTRIES} entries). Flush before adding more.`,
|
|
1107
|
+
2
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
const newEntry = {
|
|
1111
|
+
id: crypto.randomUUID(),
|
|
1112
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1113
|
+
tool: entry.tool,
|
|
1114
|
+
args: { ...entry.args }
|
|
1115
|
+
};
|
|
1116
|
+
const line = JSON.stringify(newEntry) + "\n";
|
|
1117
|
+
try {
|
|
1118
|
+
await appendFile(queueFilePath(projectDir), line, "utf8");
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
if (err instanceof CliError) throw err;
|
|
1121
|
+
throw new CliError(
|
|
1122
|
+
`Failed to write queue entry: ${err.message ?? String(err)}`,
|
|
1123
|
+
2
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
return newEntry;
|
|
1127
|
+
}
|
|
1128
|
+
async function atomicRewriteQueue(projectDir, entries) {
|
|
1129
|
+
const filePath = queueFilePath(projectDir);
|
|
1130
|
+
if (entries.length === 0) {
|
|
1131
|
+
try {
|
|
1132
|
+
await unlink(filePath);
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
const code = err.code;
|
|
1135
|
+
if (code !== "ENOENT") throw err;
|
|
1136
|
+
}
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const noltoDir = path4.join(projectDir, ".nolto");
|
|
1140
|
+
await mkdir2(noltoDir, { recursive: true });
|
|
1141
|
+
const tmpPath = filePath + ".tmp";
|
|
1142
|
+
const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
1143
|
+
await writeFile2(tmpPath, content, "utf8");
|
|
1144
|
+
renameSync(tmpPath, filePath);
|
|
1145
|
+
}
|
|
1146
|
+
async function acquireLock(projectDir) {
|
|
1147
|
+
const noltoDir = path4.join(projectDir, ".nolto");
|
|
1148
|
+
await mkdir2(noltoDir, { recursive: true });
|
|
1149
|
+
const lockPath = lockFilePath(projectDir);
|
|
1150
|
+
return tryAcquire(lockPath);
|
|
1151
|
+
}
|
|
1152
|
+
async function tryAcquire(lockPath) {
|
|
1153
|
+
try {
|
|
1154
|
+
const fd = openSync(lockPath, "wx");
|
|
1155
|
+
writeFileSync(fd, String(process.pid));
|
|
1156
|
+
closeSync(fd);
|
|
1157
|
+
return makeLockHandle(lockPath);
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
if (err.code !== "EEXIST") throw err;
|
|
1160
|
+
}
|
|
1161
|
+
let pidStr;
|
|
1162
|
+
try {
|
|
1163
|
+
pidStr = await readFile3(lockPath, "utf8");
|
|
1164
|
+
} catch {
|
|
1165
|
+
try {
|
|
1166
|
+
unlinkSync(lockPath);
|
|
1167
|
+
} catch {
|
|
1168
|
+
}
|
|
1169
|
+
return tryAcquire(lockPath);
|
|
1170
|
+
}
|
|
1171
|
+
const pid = parseInt(pidStr, 10);
|
|
1172
|
+
if (isNaN(pid)) {
|
|
1173
|
+
try {
|
|
1174
|
+
unlinkSync(lockPath);
|
|
1175
|
+
} catch {
|
|
1176
|
+
}
|
|
1177
|
+
return tryAcquire(lockPath);
|
|
1178
|
+
}
|
|
1179
|
+
try {
|
|
1180
|
+
process.kill(pid, 0);
|
|
1181
|
+
return null;
|
|
1182
|
+
} catch (sigErr) {
|
|
1183
|
+
if (sigErr.code === "ESRCH") {
|
|
1184
|
+
try {
|
|
1185
|
+
unlinkSync(lockPath);
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
return tryAcquire(lockPath);
|
|
1189
|
+
}
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function makeLockHandle(lockPath) {
|
|
1194
|
+
let released = false;
|
|
1195
|
+
return {
|
|
1196
|
+
async release() {
|
|
1197
|
+
if (released) return;
|
|
1198
|
+
released = true;
|
|
1199
|
+
try {
|
|
1200
|
+
unlinkSync(lockPath);
|
|
1201
|
+
} catch {
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
async function appendLog(projectDir, level, message) {
|
|
1207
|
+
try {
|
|
1208
|
+
const noltoDir = path4.join(projectDir, ".nolto");
|
|
1209
|
+
await mkdir2(noltoDir, { recursive: true });
|
|
1210
|
+
const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}
|
|
1211
|
+
`;
|
|
1212
|
+
await appendFile(logFilePath(projectDir), line, "utf8");
|
|
1213
|
+
} catch {
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/commands/queue.ts
|
|
1218
|
+
function withQueueDir(cmd) {
|
|
1219
|
+
return cmd.option("--queue-dir <path>", "Override project directory for queue files");
|
|
1220
|
+
}
|
|
1221
|
+
function registerQueue(program, deps) {
|
|
1222
|
+
const queue = program.command("queue").description("Queue a progress report for later flush.");
|
|
1223
|
+
withQueueDir(
|
|
1224
|
+
queue.command("phase-status <planId> <phaseId> <status>").description("Queue a phase status update (offline, no token required).").option("--message <text>", "Optional message")
|
|
1225
|
+
).action(
|
|
1226
|
+
async (planId, phaseId, status, opts) => {
|
|
1227
|
+
if (!PLAN_STATUSES.includes(status)) {
|
|
1228
|
+
throw new CliError(
|
|
1229
|
+
`Invalid status "${status}". Valid values: ${PLAN_STATUSES.join(", ")}`,
|
|
1230
|
+
2
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
const projectDir = resolveQueueDir({
|
|
1234
|
+
flagDir: opts.queueDir,
|
|
1235
|
+
env: process.env,
|
|
1236
|
+
cwd: process.cwd()
|
|
1237
|
+
});
|
|
1238
|
+
const args = {
|
|
1239
|
+
planId,
|
|
1240
|
+
phaseId,
|
|
1241
|
+
status
|
|
1242
|
+
};
|
|
1243
|
+
if (deps.settings.defaultProjectId != null) args["projectId"] = deps.settings.defaultProjectId;
|
|
1244
|
+
if (opts.message != null) args["message"] = opts.message;
|
|
1245
|
+
const entry = await appendEntry(projectDir, { tool: "update_phase_status", args });
|
|
1246
|
+
if (deps.output.mode === "json") {
|
|
1247
|
+
printResult({ queued: true, id: entry.id }, "json");
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
process.stdout.write(`Queued update_phase_status.
|
|
1251
|
+
`);
|
|
1252
|
+
}
|
|
1253
|
+
);
|
|
1254
|
+
withQueueDir(
|
|
1255
|
+
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")
|
|
1256
|
+
).action(
|
|
1257
|
+
async (planId, phaseId, verdict, opts) => {
|
|
1258
|
+
if (!TEST_VERDICTS.includes(verdict)) {
|
|
1259
|
+
throw new CliError(
|
|
1260
|
+
`Invalid verdict "${verdict}". Valid values: ${TEST_VERDICTS.join(", ")}`,
|
|
1261
|
+
2
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
let round;
|
|
1265
|
+
if (opts.round != null) {
|
|
1266
|
+
if (!/^[1-9]\d*$/.test(opts.round)) {
|
|
1267
|
+
throw new CliError(`--round must be a positive integer, got "${opts.round}"`, 2);
|
|
1268
|
+
}
|
|
1269
|
+
const parsed = Number(opts.round);
|
|
1270
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
1271
|
+
throw new CliError(`--round must be a positive integer, got "${opts.round}"`, 2);
|
|
1272
|
+
}
|
|
1273
|
+
round = parsed;
|
|
1274
|
+
}
|
|
1275
|
+
const projectDir = resolveQueueDir({
|
|
1276
|
+
flagDir: opts.queueDir,
|
|
1277
|
+
env: process.env,
|
|
1278
|
+
cwd: process.cwd()
|
|
1279
|
+
});
|
|
1280
|
+
const args = {
|
|
1281
|
+
planId,
|
|
1282
|
+
phaseId,
|
|
1283
|
+
verdict
|
|
1284
|
+
};
|
|
1285
|
+
if (deps.settings.defaultProjectId != null) args["projectId"] = deps.settings.defaultProjectId;
|
|
1286
|
+
if (round != null) args["round"] = round;
|
|
1287
|
+
if (opts.summary != null) args["summary"] = opts.summary;
|
|
1288
|
+
const entry = await appendEntry(projectDir, { tool: "record_phase_test_result", args });
|
|
1289
|
+
if (deps.output.mode === "json") {
|
|
1290
|
+
printResult({ queued: true, id: entry.id }, "json");
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
process.stdout.write(`Queued record_phase_test_result.
|
|
1294
|
+
`);
|
|
1295
|
+
}
|
|
1296
|
+
);
|
|
1297
|
+
withQueueDir(
|
|
1298
|
+
queue.command("plan-status <planId> <status>").description("Queue a plan status update (offline, no token required).").option("--message <text>", "Optional message")
|
|
1299
|
+
).action(
|
|
1300
|
+
async (planId, status, opts) => {
|
|
1301
|
+
if (!PLAN_STATUSES.includes(status)) {
|
|
1302
|
+
throw new CliError(
|
|
1303
|
+
`Invalid status "${status}". Valid values: ${PLAN_STATUSES.join(", ")}`,
|
|
1304
|
+
2
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
const projectDir = resolveQueueDir({
|
|
1308
|
+
flagDir: opts.queueDir,
|
|
1309
|
+
env: process.env,
|
|
1310
|
+
cwd: process.cwd()
|
|
1311
|
+
});
|
|
1312
|
+
const args = {
|
|
1313
|
+
planId,
|
|
1314
|
+
status
|
|
1315
|
+
};
|
|
1316
|
+
if (deps.settings.defaultProjectId != null) args["projectId"] = deps.settings.defaultProjectId;
|
|
1317
|
+
if (opts.message != null) args["message"] = opts.message;
|
|
1318
|
+
const entry = await appendEntry(projectDir, { tool: "update_plan_status", args });
|
|
1319
|
+
if (deps.output.mode === "json") {
|
|
1320
|
+
printResult({ queued: true, id: entry.id }, "json");
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
process.stdout.write(`Queued update_plan_status.
|
|
1324
|
+
`);
|
|
1325
|
+
}
|
|
1326
|
+
);
|
|
1327
|
+
withQueueDir(
|
|
1328
|
+
queue.command("plan-review <planId> <verdict>").description("Queue a plan review (offline, no token required).").option("--summary <text>", "Review summary")
|
|
1329
|
+
).action(
|
|
1330
|
+
async (planId, verdict, opts) => {
|
|
1331
|
+
if (!REVIEW_VERDICTS.includes(verdict)) {
|
|
1332
|
+
throw new CliError(
|
|
1333
|
+
`Invalid verdict "${verdict}". Valid values: ${REVIEW_VERDICTS.join(", ")}`,
|
|
1334
|
+
2
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
const projectDir = resolveQueueDir({
|
|
1338
|
+
flagDir: opts.queueDir,
|
|
1339
|
+
env: process.env,
|
|
1340
|
+
cwd: process.cwd()
|
|
1341
|
+
});
|
|
1342
|
+
const args = {
|
|
1343
|
+
planId,
|
|
1344
|
+
verdict
|
|
1345
|
+
};
|
|
1346
|
+
if (deps.settings.defaultProjectId != null) args["projectId"] = deps.settings.defaultProjectId;
|
|
1347
|
+
if (opts.summary != null) args["summary"] = opts.summary;
|
|
1348
|
+
const entry = await appendEntry(projectDir, { tool: "record_plan_review", args });
|
|
1349
|
+
if (deps.output.mode === "json") {
|
|
1350
|
+
printResult({ queued: true, id: entry.id }, "json");
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
process.stdout.write(`Queued record_plan_review.
|
|
1354
|
+
`);
|
|
1355
|
+
}
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// src/detach.ts
|
|
1360
|
+
import { spawn } from "child_process";
|
|
1361
|
+
function spawnDetachedFlush(scriptPath, queueDir) {
|
|
1362
|
+
const extraArgs = queueDir != null ? ["--queue-dir", queueDir] : [];
|
|
1363
|
+
const child = spawn(
|
|
1364
|
+
process.execPath,
|
|
1365
|
+
[scriptPath, "flush", ...extraArgs],
|
|
1366
|
+
{
|
|
1367
|
+
detached: true,
|
|
1368
|
+
stdio: "ignore",
|
|
1369
|
+
env: process.env
|
|
1370
|
+
}
|
|
1371
|
+
);
|
|
1372
|
+
child.unref();
|
|
1373
|
+
return child.pid;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// src/commands/flush.ts
|
|
1377
|
+
async function runFlushWorker(deps, projectDir) {
|
|
1378
|
+
const lock = await acquireLock(projectDir);
|
|
1379
|
+
if (lock === null) {
|
|
1380
|
+
process.stdout.write("Another flush is running.\n");
|
|
1381
|
+
return { flushed: 0, remaining: 0, failed: 0 };
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
const entries = await readQueue(projectDir);
|
|
1385
|
+
if (entries.length === 0) {
|
|
1386
|
+
process.stdout.write("Nothing to flush.\n");
|
|
1387
|
+
return { flushed: 0, remaining: 0, failed: 0 };
|
|
1388
|
+
}
|
|
1389
|
+
if (deps.settings.token == null) {
|
|
1390
|
+
await appendLog(projectDir, "warn", "flush skipped: no token configured");
|
|
1391
|
+
return { flushed: 0, remaining: entries.length, failed: 0 };
|
|
1392
|
+
}
|
|
1393
|
+
let flushed = 0;
|
|
1394
|
+
let remaining = entries.slice();
|
|
1395
|
+
for (const entry of entries) {
|
|
1396
|
+
try {
|
|
1397
|
+
await deps.caller.call(entry.tool, entry.args);
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
const cliErr = err instanceof CliError ? err : new CliError(String(err), 5);
|
|
1400
|
+
await appendLog(
|
|
1401
|
+
projectDir,
|
|
1402
|
+
"error",
|
|
1403
|
+
`${entry.tool} id=${entry.id} exit=${cliErr.exitCode} ${cliErr.message}`
|
|
1404
|
+
);
|
|
1405
|
+
return { flushed, remaining: remaining.length, failed: 1 };
|
|
1406
|
+
}
|
|
1407
|
+
flushed++;
|
|
1408
|
+
remaining = remaining.slice(1);
|
|
1409
|
+
await atomicRewriteQueue(projectDir, remaining);
|
|
1410
|
+
}
|
|
1411
|
+
return { flushed, remaining: 0, failed: 0 };
|
|
1412
|
+
} finally {
|
|
1413
|
+
await lock.release();
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function register7(program, deps) {
|
|
1417
|
+
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) => {
|
|
1418
|
+
const projectDir = resolveQueueDir({
|
|
1419
|
+
flagDir: opts.queueDir,
|
|
1420
|
+
env: process.env,
|
|
1421
|
+
cwd: process.cwd()
|
|
1422
|
+
});
|
|
1423
|
+
if (opts.detach) {
|
|
1424
|
+
spawnDetachedFlush(process.argv[1], opts.queueDir);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
try {
|
|
1428
|
+
const summary = await runFlushWorker(deps, projectDir);
|
|
1429
|
+
if (deps.output.mode === "json") {
|
|
1430
|
+
process.stdout.write(JSON.stringify(summary) + "\n");
|
|
1431
|
+
} else {
|
|
1432
|
+
if (summary.flushed > 0 || summary.failed > 0) {
|
|
1433
|
+
process.stdout.write(
|
|
1434
|
+
`Flushed ${summary.flushed}, remaining ${summary.remaining}, failed ${summary.failed}.
|
|
1435
|
+
`
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1441
|
+
await appendLog(projectDir, "error", `unexpected flush error: ${msg}`);
|
|
1442
|
+
process.stderr.write(`Flush error (logged to .nolto/flush.log): ${msg}
|
|
1443
|
+
`);
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1026
1448
|
// src/program.ts
|
|
1027
1449
|
function stripCommanderErrorPrefix(msg) {
|
|
1028
1450
|
return msg.startsWith("error: ") ? msg.slice("error: ".length) : msg;
|
|
@@ -1037,15 +1459,17 @@ function buildProgram(deps) {
|
|
|
1037
1459
|
register4(program, deps);
|
|
1038
1460
|
register5(program, deps);
|
|
1039
1461
|
register6(program, deps);
|
|
1462
|
+
registerQueue(program, deps);
|
|
1463
|
+
register7(program, deps);
|
|
1040
1464
|
return program;
|
|
1041
1465
|
}
|
|
1042
1466
|
|
|
1043
1467
|
// src/index.ts
|
|
1044
|
-
var __dirname2 =
|
|
1468
|
+
var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
|
|
1045
1469
|
var require2 = createRequire2(import.meta.url);
|
|
1046
1470
|
function getVersion() {
|
|
1047
1471
|
try {
|
|
1048
|
-
const pkgPath =
|
|
1472
|
+
const pkgPath = path5.resolve(__dirname2, "../package.json");
|
|
1049
1473
|
const pkg = require2(pkgPath);
|
|
1050
1474
|
return pkg.version ?? "0.0.0";
|
|
1051
1475
|
} catch {
|