syntaur 0.1.2 → 0.1.5

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 CHANGED
@@ -114,3 +114,31 @@ Repo-local plugin linking for development:
114
114
  npx syntaur@latest install-plugin --link
115
115
  npx syntaur@latest install-codex-plugin --link
116
116
  ```
117
+
118
+ ## Release Publishing
119
+
120
+ This repo is set up for npm trusted publishing from GitHub Actions.
121
+
122
+ Release flow:
123
+
124
+ ```bash
125
+ npm version patch
126
+ git push origin main
127
+ git push origin v$(node -p "require('./package.json').version")
128
+ ```
129
+
130
+ The publish workflow lives at `.github/workflows/publish.yml` and only runs on version tags like `v0.1.4`. It checks that the tag matches `package.json`, runs the repo validation, and then publishes to npm using GitHub OIDC instead of a long-lived npm token.
131
+
132
+ One-time npm setup:
133
+
134
+ - package: `syntaur`
135
+ - GitHub repo: `prong-horn/syntaur`
136
+ - workflow filename: `publish.yml`
137
+
138
+ You can configure the trusted publisher either in the npm package settings UI or with npm CLI `11.10+`:
139
+
140
+ ```bash
141
+ npx npm@^11.10.0 trust github syntaur --repo prong-horn/syntaur --file publish.yml -y
142
+ ```
143
+
144
+ After trusted publishing is working, npm recommends switching the package publishing access to `Require two-factor authentication and disallow tokens`.
package/dist/index.js CHANGED
@@ -4773,6 +4773,7 @@ Use --slug to specify a different slug.`
4773
4773
  // src/commands/dashboard.ts
4774
4774
  init_config2();
4775
4775
  import { spawn } from "child_process";
4776
+ import { createServer as createNetServer } from "net";
4776
4777
  import { resolve as resolve18, dirname as dirname5 } from "path";
4777
4778
  import { fileURLToPath as fileURLToPath3 } from "url";
4778
4779
 
@@ -7234,14 +7235,51 @@ function resolveDashboardMode(options) {
7234
7235
  }
7235
7236
  return "static";
7236
7237
  }
7238
+ async function isPortAvailable(port) {
7239
+ return new Promise((resolveAvailability) => {
7240
+ const tester = createNetServer();
7241
+ tester.once("error", () => {
7242
+ resolveAvailability(false);
7243
+ });
7244
+ tester.once("listening", () => {
7245
+ tester.close(() => resolveAvailability(true));
7246
+ });
7247
+ tester.listen(port, "127.0.0.1");
7248
+ });
7249
+ }
7250
+ async function findAvailablePort(startPort, maxAttempts = 20) {
7251
+ for (let offset = 0; offset < maxAttempts; offset += 1) {
7252
+ const candidate = startPort + offset;
7253
+ if (candidate > 65535) {
7254
+ break;
7255
+ }
7256
+ if (await isPortAvailable(candidate)) {
7257
+ return candidate;
7258
+ }
7259
+ }
7260
+ return null;
7261
+ }
7237
7262
  async function dashboardCommand(options) {
7238
7263
  const config = await readConfig();
7239
7264
  const missionsDir = config.defaultMissionDir;
7240
- const port = parseInt(options.port, 10);
7241
- if (isNaN(port) || port < 1 || port > 65535) {
7265
+ const requestedPort = parseInt(options.port, 10);
7266
+ if (isNaN(requestedPort) || requestedPort < 1 || requestedPort > 65535) {
7242
7267
  throw new Error(`Invalid port "${options.port}". Must be a number between 1 and 65535.`);
7243
7268
  }
7244
7269
  const mode = resolveDashboardMode(options);
7270
+ let port = requestedPort;
7271
+ if (options.autoPort) {
7272
+ const availablePort = await findAvailablePort(requestedPort);
7273
+ if (availablePort === null) {
7274
+ throw new Error(
7275
+ `Could not find an available dashboard port starting at ${requestedPort}. Run "syntaur dashboard --port <number>" to choose one manually.`
7276
+ );
7277
+ }
7278
+ if (availablePort !== requestedPort) {
7279
+ console.log(`Port ${requestedPort} is busy. Launching the dashboard on port ${availablePort} instead.`);
7280
+ }
7281
+ port = availablePort;
7282
+ }
7245
7283
  const server = createDashboardServer({
7246
7284
  port,
7247
7285
  missionsDir,
@@ -8343,6 +8381,19 @@ import { createInterface } from "readline/promises";
8343
8381
  function isInteractiveTerminal() {
8344
8382
  return Boolean(input.isTTY && output.isTTY);
8345
8383
  }
8384
+ function parseConfirmAnswer(answer, defaultValue = false) {
8385
+ const normalized = answer.trim().toLowerCase();
8386
+ if (normalized === "") {
8387
+ return defaultValue;
8388
+ }
8389
+ if (normalized === "y" || normalized === "yes") {
8390
+ return true;
8391
+ }
8392
+ if (normalized === "n" || normalized === "no") {
8393
+ return false;
8394
+ }
8395
+ return null;
8396
+ }
8346
8397
  async function confirmPrompt(question, defaultValue = false) {
8347
8398
  if (!isInteractiveTerminal()) {
8348
8399
  throw new Error("Interactive confirmation requires a TTY.");
@@ -8350,11 +8401,14 @@ async function confirmPrompt(question, defaultValue = false) {
8350
8401
  const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
8351
8402
  const rl = createInterface({ input, output });
8352
8403
  try {
8353
- const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
8354
- if (answer === "") {
8355
- return defaultValue;
8404
+ while (true) {
8405
+ const answer = await rl.question(`${question}${suffix}`);
8406
+ const parsed = parseConfirmAnswer(answer, defaultValue);
8407
+ if (parsed !== null) {
8408
+ return parsed;
8409
+ }
8410
+ console.log("Enter y, yes, n, no, or press Enter for the default.");
8356
8411
  }
8357
- return answer === "y" || answer === "yes";
8358
8412
  } finally {
8359
8413
  rl.close();
8360
8414
  }
@@ -8591,7 +8645,7 @@ async function setupCommand(options) {
8591
8645
  targetDir: options.claudeDir,
8592
8646
  promptForTarget: !options.yes
8593
8647
  });
8594
- } else if (!interactive && !options.yes && !initialized) {
8648
+ } else {
8595
8649
  console.log(`Skip Claude plugin for now. Install later with: ${getPluginInstallCommand("claude")}`);
8596
8650
  }
8597
8651
  if (installCodex) {
@@ -8600,12 +8654,22 @@ async function setupCommand(options) {
8600
8654
  marketplacePath: options.codexMarketplacePath,
8601
8655
  promptForTarget: !options.yes
8602
8656
  });
8603
- } else if (!interactive && !options.yes && !initialized) {
8657
+ } else {
8604
8658
  console.log(`Skip Codex plugin for now. Install later with: ${getPluginInstallCommand("codex")}`);
8605
8659
  }
8606
8660
  if (launchDashboard) {
8661
+ const preferredPort = 4800;
8662
+ const port = await findAvailablePort(preferredPort);
8663
+ if (port === null) {
8664
+ throw new Error(
8665
+ `Could not find an available dashboard port starting at ${preferredPort}. Run "syntaur dashboard --port <number>" to choose one manually.`
8666
+ );
8667
+ }
8668
+ if (port !== preferredPort) {
8669
+ console.log(`Port ${preferredPort} is busy. Launching the dashboard on port ${port} instead.`);
8670
+ }
8607
8671
  await dashboardCommand({
8608
- port: "4800",
8672
+ port: String(port),
8609
8673
  dev: false,
8610
8674
  serverOnly: false,
8611
8675
  apiOnly: false,
@@ -9384,9 +9448,13 @@ program.command("create-assignment").description("Create a new assignment within
9384
9448
  process.exit(1);
9385
9449
  }
9386
9450
  });
9387
- program.command("dashboard").description("Start the local Syntaur dashboard web UI").option("--port <number>", "Port to run the dashboard on", "4800").option("--dev", "Run the dashboard with the Vite dev server", false).option("--server-only", "Run only the API server without any UI", false).option("--api-only", "Deprecated alias for --server-only", false).option("--no-open", "Do not automatically open the browser").action(async (options) => {
9451
+ program.command("dashboard").description("Start the local Syntaur dashboard web UI").option("--port <number>", "Port to run the dashboard on", "4800").option("--dev", "Run the dashboard with the Vite dev server", false).option("--server-only", "Run only the API server without any UI", false).option("--api-only", "Deprecated alias for --server-only", false).option("--no-open", "Do not automatically open the browser").action(async (options, command) => {
9388
9452
  try {
9389
- await dashboardCommand(options);
9453
+ const autoPort = command.getOptionValueSource("port") !== "cli";
9454
+ await dashboardCommand({
9455
+ ...options,
9456
+ autoPort
9457
+ });
9390
9458
  } catch (error) {
9391
9459
  console.error(
9392
9460
  "Error:",