neuvikon 0.2.16 → 0.2.18
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 -0
- package/dist/index.mjs +322 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ as being worked on, by whom, with which tool.
|
|
|
36
36
|
| `neuvikon login` | Sign in through the browser and set up the current project |
|
|
37
37
|
| `neuvikon init` | Set up another project (credentials are per-machine, rules are per-project) |
|
|
38
38
|
| `neuvikon start --ticket EWWQ-1` | Begin tracking. `--tool claude-code\|codex\|cursor\|other` |
|
|
39
|
+
| `neuvikon start --tickets EWWQ-1,EWWQ-2 --spawn` | Open one terminal per ticket instead of tracking one here |
|
|
39
40
|
| `neuvikon stop` | Stop tracking |
|
|
40
41
|
| `neuvikon status` | Login, project setup, current ticket |
|
|
41
42
|
| `neuvikon logout` | Revoke this machine's token and forget it |
|
|
@@ -48,6 +49,26 @@ as being worked on, by whom, with which tool.
|
|
|
48
49
|
pbpaste | npx neuvikon start
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
## Kicking off several tickets at once
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npx neuvikon start --tickets EWWQ-1,EWWQ-2,EWWQ-3 --spawn
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Opens one terminal per ticket (a tmux window if you're already in tmux, an
|
|
59
|
+
iTerm2 or Terminal.app tab on macOS, a Windows Terminal tab on Windows), each
|
|
60
|
+
`cd`'d into this directory. If a coding agent CLI is installed (Claude Code,
|
|
61
|
+
Cursor, Codex) it's launched in each terminal with that ticket's full text as
|
|
62
|
+
its first prompt — same substance a manual paste would give it. With more
|
|
63
|
+
than one installed, you're asked once which to use and that choice is
|
|
64
|
+
remembered (`--agent <name>` overrides it any time). Nothing is tracked
|
|
65
|
+
automatically, even then: a session only scopes safely once an agent is
|
|
66
|
+
actually running, so the launched agent runs `npx neuvikon start --ticket
|
|
67
|
+
EWWQ-1` itself once it's up, via its rules file, same as a manual paste
|
|
68
|
+
always has. Without a detected agent, or on a platform/terminal this can't
|
|
69
|
+
drive, it just prints the ticket ref and that command for you to paste by
|
|
70
|
+
hand.
|
|
71
|
+
|
|
51
72
|
## Project pages
|
|
52
73
|
|
|
53
74
|
An agent's most useful output often isn't the ticket, it's what it learned
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { delimiter, dirname, join } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
6
7
|
//#region src/api.ts
|
|
7
8
|
var ApiError = class extends Error {
|
|
8
9
|
status;
|
|
@@ -79,6 +80,23 @@ function resolveDeployment(flags) {
|
|
|
79
80
|
const DEFAULT_SITE_URL = process.env.NEUVIKON_URL ?? DEPLOYMENTS.production.api;
|
|
80
81
|
const CONFIG_DIR = join(homedir(), ".neuvikon");
|
|
81
82
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
83
|
+
function readConfigFile() {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
86
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
87
|
+
} catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function writeConfigFile(config) {
|
|
92
|
+
mkdirSync(CONFIG_DIR, {
|
|
93
|
+
recursive: true,
|
|
94
|
+
mode: 448
|
|
95
|
+
});
|
|
96
|
+
chmodSync(CONFIG_DIR, 448);
|
|
97
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
|
|
98
|
+
chmodSync(CONFIG_PATH, 384);
|
|
99
|
+
}
|
|
82
100
|
/**
|
|
83
101
|
* Somewhere a browser cannot open — CI, a container, a remote box — there is no
|
|
84
102
|
* way to complete a device login, so a token handed in through the environment
|
|
@@ -116,16 +134,15 @@ function readCredentials() {
|
|
|
116
134
|
/**
|
|
117
135
|
* A token on disk is only as safe as its permissions, so directory and file are
|
|
118
136
|
* locked to the owner on every write, not just on first create — an earlier
|
|
119
|
-
* version may have left them readable.
|
|
137
|
+
* version may have left them readable. Merges over the existing file rather
|
|
138
|
+
* than replacing it outright, so a re-login doesn't silently drop unrelated
|
|
139
|
+
* settings this file has grown since (e.g. `preferredAgent`).
|
|
120
140
|
*/
|
|
121
141
|
function writeCredentials(credentials) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
writeConfigFile({
|
|
143
|
+
...readConfigFile(),
|
|
144
|
+
...credentials
|
|
125
145
|
});
|
|
126
|
-
chmodSync(CONFIG_DIR, 448);
|
|
127
|
-
writeFileSync(CONFIG_PATH, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
|
|
128
|
-
chmodSync(CONFIG_PATH, 384);
|
|
129
146
|
}
|
|
130
147
|
function clearCredentials() {
|
|
131
148
|
try {
|
|
@@ -135,6 +152,22 @@ function clearCredentials() {
|
|
|
135
152
|
return false;
|
|
136
153
|
}
|
|
137
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Which agent CLI `start --spawn` should launch without asking, remembered
|
|
157
|
+
* across invocations once the user has answered the question once. Kept in
|
|
158
|
+
* the same config file as credentials rather than a separate one — it's the
|
|
159
|
+
* same "settings for this machine" concern.
|
|
160
|
+
*/
|
|
161
|
+
function readPreferredAgent() {
|
|
162
|
+
const value = readConfigFile().preferredAgent;
|
|
163
|
+
return typeof value === "string" && value ? value : null;
|
|
164
|
+
}
|
|
165
|
+
function writePreferredAgent(id) {
|
|
166
|
+
writeConfigFile({
|
|
167
|
+
...readConfigFile(),
|
|
168
|
+
preferredAgent: id
|
|
169
|
+
});
|
|
170
|
+
}
|
|
138
171
|
//#endregion
|
|
139
172
|
//#region src/session.ts
|
|
140
173
|
/**
|
|
@@ -1134,6 +1167,210 @@ async function pages(flags, positional) {
|
|
|
1134
1167
|
}
|
|
1135
1168
|
}
|
|
1136
1169
|
//#endregion
|
|
1170
|
+
//#region src/agentCli.ts
|
|
1171
|
+
const KNOWN_AGENT_CLIS = [
|
|
1172
|
+
{
|
|
1173
|
+
id: "claude-code",
|
|
1174
|
+
label: "Claude Code",
|
|
1175
|
+
binary: "claude",
|
|
1176
|
+
buildArgs: (prompt) => [prompt]
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
id: "cursor",
|
|
1180
|
+
label: "Cursor",
|
|
1181
|
+
binary: "cursor-agent",
|
|
1182
|
+
buildArgs: (prompt) => [prompt]
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
id: "codex",
|
|
1186
|
+
label: "Codex",
|
|
1187
|
+
binary: "codex",
|
|
1188
|
+
buildArgs: (prompt) => [prompt]
|
|
1189
|
+
}
|
|
1190
|
+
];
|
|
1191
|
+
/**
|
|
1192
|
+
* Manual PATH scan instead of shelling out to `which`/`where`: keeps this
|
|
1193
|
+
* dependency-free and synchronous, and doesn't assume either helper exists
|
|
1194
|
+
* (minimal containers, and Windows has neither `which` nor always `where`
|
|
1195
|
+
* usable from a spawned child the same way).
|
|
1196
|
+
*/
|
|
1197
|
+
function isOnPath(binary) {
|
|
1198
|
+
const pathEnv = process.env.PATH ?? "";
|
|
1199
|
+
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
1200
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
1201
|
+
if (!dir) continue;
|
|
1202
|
+
for (const ext of exts) if (existsSync(join(dir, binary + ext))) return true;
|
|
1203
|
+
}
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
function detectAgentClis() {
|
|
1207
|
+
return KNOWN_AGENT_CLIS.filter((agent) => isOnPath(agent.binary));
|
|
1208
|
+
}
|
|
1209
|
+
function findById(id) {
|
|
1210
|
+
return KNOWN_AGENT_CLIS.find((agent) => agent.id === id);
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Only prompts when stdin is a real terminal: `--tickets`/`--spawn` never
|
|
1214
|
+
* reads stdin for the ticket itself (unlike plain `start`), so it's free for
|
|
1215
|
+
* this, but a non-interactive invocation (CI, a script) must never block on
|
|
1216
|
+
* input that will never arrive.
|
|
1217
|
+
*/
|
|
1218
|
+
async function promptForAgent(candidates) {
|
|
1219
|
+
if (!process.stdin.isTTY) return candidates[0] ?? null;
|
|
1220
|
+
info("Multiple coding agents found on this machine:");
|
|
1221
|
+
candidates.forEach((agent, index) => info(` ${index + 1}. ${agent.label}`));
|
|
1222
|
+
info(dim("This choice is remembered — override any time with --agent <name>."));
|
|
1223
|
+
const rl = createInterface({
|
|
1224
|
+
input: process.stdin,
|
|
1225
|
+
output: process.stderr
|
|
1226
|
+
});
|
|
1227
|
+
try {
|
|
1228
|
+
for (;;) {
|
|
1229
|
+
const answer = (await rl.question(`Which one should Neuvikon launch? [1-${candidates.length}]: `)).trim();
|
|
1230
|
+
const picked = candidates[Number(answer) - 1];
|
|
1231
|
+
if (picked) return picked;
|
|
1232
|
+
info(dim(`Enter a number from 1 to ${candidates.length}.`));
|
|
1233
|
+
}
|
|
1234
|
+
} finally {
|
|
1235
|
+
rl.close();
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Resolves which agent CLI (if any) `--spawn` should launch. `--agent` is a
|
|
1240
|
+
* one-off override and is never persisted, same as `--tool` never rewrites
|
|
1241
|
+
* anything. Otherwise: a stored preference wins as long as it's still on
|
|
1242
|
+
* this machine's PATH (an uninstalled tool shouldn't stay sticky forever);
|
|
1243
|
+
* failing that, one candidate is used silently, and more than one is asked
|
|
1244
|
+
* about exactly once and then remembered.
|
|
1245
|
+
*/
|
|
1246
|
+
async function resolveAgentCli(flags) {
|
|
1247
|
+
const explicit = flags.get("agent");
|
|
1248
|
+
if (explicit) {
|
|
1249
|
+
const agent = findById(explicit);
|
|
1250
|
+
if (!agent) fail(`Unknown agent "${explicit}".`, `Expected one of: ${KNOWN_AGENT_CLIS.map((a) => a.id).join(", ")}.`);
|
|
1251
|
+
return agent;
|
|
1252
|
+
}
|
|
1253
|
+
const candidates = detectAgentClis();
|
|
1254
|
+
if (candidates.length === 0) return null;
|
|
1255
|
+
const preferredId = readPreferredAgent();
|
|
1256
|
+
if (preferredId) {
|
|
1257
|
+
const preferred = candidates.find((agent) => agent.id === preferredId);
|
|
1258
|
+
if (preferred) return preferred;
|
|
1259
|
+
}
|
|
1260
|
+
const picked = candidates.length === 1 ? candidates[0] : await promptForAgent(candidates);
|
|
1261
|
+
if (picked) writePreferredAgent(picked.id);
|
|
1262
|
+
return picked;
|
|
1263
|
+
}
|
|
1264
|
+
//#endregion
|
|
1265
|
+
//#region src/terminal.ts
|
|
1266
|
+
function shQuote(value) {
|
|
1267
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Never `exec`s the agent in place: if it's the wrong binary, isn't actually
|
|
1271
|
+
* installed, or the user just exits it, the terminal must not go on to close
|
|
1272
|
+
* or hang — it drops to the same interactive shell either way, ticket ref
|
|
1273
|
+
* already printed above.
|
|
1274
|
+
*
|
|
1275
|
+
* Still deliberately does not run `neuvikon start` itself: that only scopes
|
|
1276
|
+
* safely once an agent is attached (see session.ts's instanceKey), and
|
|
1277
|
+
* nothing is attached yet in a terminal that just opened. Launching the
|
|
1278
|
+
* agent (rather than `start`) is safe precisely because the agent is the one
|
|
1279
|
+
* that calls `start`, once it's actually running, via the rules file — same
|
|
1280
|
+
* as if the ticket had been pasted in by hand.
|
|
1281
|
+
*/
|
|
1282
|
+
function buildInnerCommand({ ref, cwd, launch }) {
|
|
1283
|
+
const lines = [`Ticket: ${ref}`];
|
|
1284
|
+
if (!launch) lines.push(`Paste it into your agent, or run: npx neuvikon start --ticket ${ref}`);
|
|
1285
|
+
const echoes = lines.map((line) => `echo ${shQuote(line)}`).join(" && ");
|
|
1286
|
+
const launchCmd = launch ? `${launch.binary} ${launch.args.map(shQuote).join(" ")}; ` : "";
|
|
1287
|
+
return `cd ${shQuote(cwd)} && ${echoes} && ${launchCmd}exec $SHELL -l`;
|
|
1288
|
+
}
|
|
1289
|
+
function escapeAppleScript(value) {
|
|
1290
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Same shape as openBrowser: fire-and-forget, never throws. A `false` return
|
|
1294
|
+
* means the spawn call itself failed synchronously (e.g. the binary isn't on
|
|
1295
|
+
* PATH) — the async case (binary exists but the app itself errors) is
|
|
1296
|
+
* swallowed by the "error" listener, same tradeoff openBrowser already makes.
|
|
1297
|
+
*/
|
|
1298
|
+
function fireAndForget(command, args) {
|
|
1299
|
+
try {
|
|
1300
|
+
const child = spawn(command, args, {
|
|
1301
|
+
stdio: "ignore",
|
|
1302
|
+
detached: true
|
|
1303
|
+
});
|
|
1304
|
+
child.on("error", () => {});
|
|
1305
|
+
child.unref();
|
|
1306
|
+
return true;
|
|
1307
|
+
} catch {
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
function spawnViaTmux(target) {
|
|
1312
|
+
return fireAndForget("tmux", [
|
|
1313
|
+
"new-window",
|
|
1314
|
+
"-n",
|
|
1315
|
+
target.ref,
|
|
1316
|
+
"-c",
|
|
1317
|
+
target.cwd,
|
|
1318
|
+
buildInnerCommand(target)
|
|
1319
|
+
]);
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* iTerm2 needs its own dialect (Terminal.app's `do script` isn't understood
|
|
1323
|
+
* by it) — detected through TERM_PROGRAM rather than "is it running", because
|
|
1324
|
+
* a user who has iTerm2 installed but is currently in Terminal.app wants a
|
|
1325
|
+
* Terminal.app tab, not a background iTerm2 window they won't notice.
|
|
1326
|
+
*/
|
|
1327
|
+
function spawnOnDarwin(target) {
|
|
1328
|
+
const inner = escapeAppleScript(buildInnerCommand(target));
|
|
1329
|
+
return fireAndForget("osascript", ["-e", process.env.TERM_PROGRAM === "iTerm.app" ? `tell application "iTerm2"
|
|
1330
|
+
activate
|
|
1331
|
+
try
|
|
1332
|
+
tell current window to create tab with default profile
|
|
1333
|
+
on error
|
|
1334
|
+
create window with default profile
|
|
1335
|
+
end try
|
|
1336
|
+
tell current window to tell current tab to tell current session to write text "${inner}"
|
|
1337
|
+
end tell` : `tell application "Terminal" to do script "${inner}"`]);
|
|
1338
|
+
}
|
|
1339
|
+
function spawnOnWindows(target) {
|
|
1340
|
+
const inner = buildInnerCommand(target).replace(/"/g, "\\\"");
|
|
1341
|
+
return fireAndForget("wt", [
|
|
1342
|
+
"-w",
|
|
1343
|
+
"0",
|
|
1344
|
+
"nt",
|
|
1345
|
+
"-d",
|
|
1346
|
+
target.cwd,
|
|
1347
|
+
"powershell",
|
|
1348
|
+
"-NoExit",
|
|
1349
|
+
"-Command",
|
|
1350
|
+
inner
|
|
1351
|
+
]);
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* `x-terminal-emulator` is the Debian/Ubuntu alternatives symlink to
|
|
1355
|
+
* whichever terminal the user has set as default — the closest thing Linux
|
|
1356
|
+
* has to a single common denominator. Anything else degrades to the plain
|
|
1357
|
+
* command list the caller already prints.
|
|
1358
|
+
*/
|
|
1359
|
+
function spawnOnLinux(target) {
|
|
1360
|
+
return fireAndForget("x-terminal-emulator", [
|
|
1361
|
+
"-e",
|
|
1362
|
+
"bash",
|
|
1363
|
+
"-c",
|
|
1364
|
+
`${buildInnerCommand(target)}; exec bash`
|
|
1365
|
+
]);
|
|
1366
|
+
}
|
|
1367
|
+
function spawnTerminal(target) {
|
|
1368
|
+
if (process.env.TMUX) return spawnViaTmux(target);
|
|
1369
|
+
if (process.platform === "darwin") return spawnOnDarwin(target);
|
|
1370
|
+
if (process.platform === "win32") return spawnOnWindows(target);
|
|
1371
|
+
return spawnOnLinux(target);
|
|
1372
|
+
}
|
|
1373
|
+
//#endregion
|
|
1137
1374
|
//#region src/commands/start.ts
|
|
1138
1375
|
const TOOLS = /* @__PURE__ */ new Set([
|
|
1139
1376
|
"claude-code",
|
|
@@ -1141,6 +1378,78 @@ const TOOLS = /* @__PURE__ */ new Set([
|
|
|
1141
1378
|
"cursor",
|
|
1142
1379
|
"other"
|
|
1143
1380
|
]);
|
|
1381
|
+
/**
|
|
1382
|
+
* Explicit and comma-separated, not scraped from stdin like a single ref
|
|
1383
|
+
* would be: a ticket's own description can easily mention other refs
|
|
1384
|
+
* ("see NS-12"), and those must never trigger opening terminals for them.
|
|
1385
|
+
*/
|
|
1386
|
+
function parseTicketsFlag(raw) {
|
|
1387
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1388
|
+
for (const token of raw.split(",")) {
|
|
1389
|
+
const trimmed = token.trim();
|
|
1390
|
+
if (!trimmed) continue;
|
|
1391
|
+
const [ref] = extractTicketRefs(trimmed);
|
|
1392
|
+
if (!ref || ref !== trimmed.toUpperCase()) fail(`"${trimmed}" is not a ticket reference.`, "Expected something like EWWQ-1.");
|
|
1393
|
+
seen.add(ref);
|
|
1394
|
+
}
|
|
1395
|
+
if (seen.size === 0) fail("No ticket references found in --tickets.", "Expected something like NS-1,NS-2.");
|
|
1396
|
+
return [...seen];
|
|
1397
|
+
}
|
|
1398
|
+
async function resolveRefs(flags, positional) {
|
|
1399
|
+
const raw = flags.get("tickets");
|
|
1400
|
+
if (raw) return parseTicketsFlag(raw);
|
|
1401
|
+
return [await resolveRef(flags, positional)];
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Best effort, same shape as `spawnTerminal` itself: a ticket that can't be
|
|
1405
|
+
* fetched (offline, a typo'd ref, a revoked token) degrades that one terminal
|
|
1406
|
+
* back to the plain print-and-shell fallback rather than failing the whole
|
|
1407
|
+
* batch, or worse, launching an agent with nothing to work from.
|
|
1408
|
+
*/
|
|
1409
|
+
async function fetchTicketText(credentials, ref) {
|
|
1410
|
+
try {
|
|
1411
|
+
return (await post(credentials.url, "/api/agent/ticket", {
|
|
1412
|
+
token: credentials.token,
|
|
1413
|
+
ticketRef: ref
|
|
1414
|
+
})).text;
|
|
1415
|
+
} catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async function buildLaunch(agent, credentials, ref) {
|
|
1420
|
+
const text = await fetchTicketText(credentials, ref);
|
|
1421
|
+
if (!text) return null;
|
|
1422
|
+
return {
|
|
1423
|
+
binary: agent.binary,
|
|
1424
|
+
args: agent.buildArgs(text)
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Opens one terminal per ref and stops there — it deliberately never calls
|
|
1429
|
+
* `start` itself. A session file written outside of a running agent has no
|
|
1430
|
+
* CLAUDE_CODE_SESSION_ID to scope it, so two of these firing in the same
|
|
1431
|
+
* directory before either agent launches would each read the other's
|
|
1432
|
+
* not-yet-attached session as "existing" and stop it out from under it. That
|
|
1433
|
+
* still holds here: a detected agent CLI is launched with the ticket's text
|
|
1434
|
+
* as its first prompt, but this never calls `start` itself — the agent does,
|
|
1435
|
+
* once it's actually running, via the rules file, exactly as if the ticket
|
|
1436
|
+
* had been pasted in by hand.
|
|
1437
|
+
*/
|
|
1438
|
+
async function spawnMode(flags, credentials, refs) {
|
|
1439
|
+
const cwd = process.cwd();
|
|
1440
|
+
const agent = await resolveAgentCli(flags);
|
|
1441
|
+
info(`Opening ${refs.length} terminal${refs.length === 1 ? "" : "s"} in ${cwd}...`);
|
|
1442
|
+
for (const ref of refs) {
|
|
1443
|
+
const launch = agent ? await buildLaunch(agent, credentials, ref) : null;
|
|
1444
|
+
const startingLabel = launch && agent ? agent.label : null;
|
|
1445
|
+
if (spawnTerminal({
|
|
1446
|
+
ref,
|
|
1447
|
+
cwd,
|
|
1448
|
+
launch
|
|
1449
|
+
})) success(startingLabel ? `${bold(ref)} — ${startingLabel} starting` : `${bold(ref)} — new terminal opened`);
|
|
1450
|
+
else info(`${bold(ref)} — couldn't open a terminal here, paste manually: ${dim(`npx neuvikon start --ticket ${ref}`)}`);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1144
1453
|
async function resolveRef(flags, positional) {
|
|
1145
1454
|
const explicit = flags.get("ticket") ?? positional[0];
|
|
1146
1455
|
if (explicit) {
|
|
@@ -1162,6 +1471,8 @@ function detectTool() {
|
|
|
1162
1471
|
async function start(flags, positional) {
|
|
1163
1472
|
const credentials = readCredentials();
|
|
1164
1473
|
if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
|
|
1474
|
+
if (flags.has("tickets") && !flags.has("spawn")) fail("--tickets needs --spawn.", "Opening more than one terminal is the only thing --tickets does.");
|
|
1475
|
+
if (flags.has("spawn")) return spawnMode(flags, credentials, await resolveRefs(flags, positional));
|
|
1165
1476
|
const ref = await resolveRef(flags, positional);
|
|
1166
1477
|
const tool = flags.get("tool") ?? detectTool();
|
|
1167
1478
|
if (!TOOLS.has(tool)) fail(`Unknown tool "${tool}".`, `Expected one of: ${[...TOOLS].join(", ")}.`);
|
|
@@ -1235,6 +1546,8 @@ ${bold("Usage")}
|
|
|
1235
1546
|
npx neuvikon login Sign in through the browser
|
|
1236
1547
|
npx neuvikon init Teach this project's coding agents to report work
|
|
1237
1548
|
npx neuvikon start --ticket EWWQ-1
|
|
1549
|
+
npx neuvikon start --tickets EWWQ-1,EWWQ-2 --spawn
|
|
1550
|
+
Open one terminal per ticket instead of tracking one here
|
|
1238
1551
|
npx neuvikon stop
|
|
1239
1552
|
npx neuvikon status Show login, project setup and current ticket
|
|
1240
1553
|
npx neuvikon logout
|
|
@@ -1256,6 +1569,7 @@ ${bold("Project pages")}
|
|
|
1256
1569
|
|
|
1257
1570
|
${bold("Options")}
|
|
1258
1571
|
--tool <name> claude-code | codex | cursor | other
|
|
1572
|
+
--agent <name> claude-code | codex | cursor — which agent CLI --spawn launches
|
|
1259
1573
|
--transcript <path> Read token usage from an agent transcript
|
|
1260
1574
|
--tokens-in <n> --tokens-out <n>
|
|
1261
1575
|
--dev Use the development deployment
|