@oss-autopilot/core 0.42.4 → 0.42.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.
@@ -12943,7 +12943,7 @@ function buildRepoMap(prs, label) {
12943
12943
  const repoMap = /* @__PURE__ */ new Map();
12944
12944
  for (const pr of prs) {
12945
12945
  if (!pr.repo) {
12946
- console.warn(`[${label}] Skipping PR #${pr.number} (${pr.url}) with empty repo field`);
12946
+ warn(label, `Skipping PR #${pr.number} (${pr.url}) with empty repo field`);
12947
12947
  continue;
12948
12948
  }
12949
12949
  const existing = repoMap.get(pr.repo) || [];
@@ -13348,6 +13348,7 @@ var init_daily_logic = __esm({
13348
13348
  "src/core/daily-logic.ts"() {
13349
13349
  "use strict";
13350
13350
  init_utils();
13351
+ init_logger();
13351
13352
  CRITICAL_STATUSES = /* @__PURE__ */ new Set([
13352
13353
  "needs_response",
13353
13354
  "needs_changes",
@@ -13947,12 +13948,13 @@ function validateGitHubUsername(username) {
13947
13948
  }
13948
13949
  return trimmed;
13949
13950
  }
13950
- var PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, MAX_URL_LENGTH, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH, USERNAME_CHARS_PATTERN, CONSECUTIVE_HYPHENS_PATTERN;
13951
+ var PR_URL_PATTERN, ISSUE_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, MAX_URL_LENGTH, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH, USERNAME_CHARS_PATTERN, CONSECUTIVE_HYPHENS_PATTERN;
13951
13952
  var init_validation = __esm({
13952
13953
  "src/commands/validation.ts"() {
13953
13954
  "use strict";
13954
13955
  init_errors();
13955
13956
  PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/;
13957
+ ISSUE_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/;
13956
13958
  ISSUE_OR_PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/\d+$/;
13957
13959
  MAX_URL_LENGTH = 2048;
13958
13960
  MAX_MESSAGE_LENGTH = 1e3;
@@ -14072,6 +14074,7 @@ __export(comments_exports, {
14072
14074
  });
14073
14075
  async function runComments(options) {
14074
14076
  validateUrl(options.prUrl);
14077
+ validateGitHubUrl(options.prUrl, PR_URL_PATTERN, "PR");
14075
14078
  const token = requireGitHubToken();
14076
14079
  const stateManager2 = getStateManager();
14077
14080
  const octokit = getOctokit(token);
@@ -14155,6 +14158,7 @@ async function runComments(options) {
14155
14158
  }
14156
14159
  async function runPost(options) {
14157
14160
  validateUrl(options.url);
14161
+ validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, "issue or PR");
14158
14162
  if (!options.message.trim()) {
14159
14163
  throw new Error("No message provided");
14160
14164
  }
@@ -14179,6 +14183,7 @@ async function runPost(options) {
14179
14183
  }
14180
14184
  async function runClaim(options) {
14181
14185
  validateUrl(options.issueUrl);
14186
+ validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
14182
14187
  const token = requireGitHubToken();
14183
14188
  const message = options.message || "Hi! I'd like to work on this issue. Could you assign it to me?";
14184
14189
  validateMessage(message);
@@ -14194,20 +14199,26 @@ async function runClaim(options) {
14194
14199
  issue_number: number,
14195
14200
  body: message
14196
14201
  });
14197
- const stateManager2 = getStateManager();
14198
- stateManager2.addIssue({
14199
- id: number,
14200
- url: options.issueUrl,
14201
- repo: `${owner}/${repo}`,
14202
- number,
14203
- title: "(claimed)",
14204
- status: "claimed",
14205
- labels: [],
14206
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
14207
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14208
- vetted: false
14209
- });
14210
- stateManager2.save();
14202
+ try {
14203
+ const stateManager2 = getStateManager();
14204
+ stateManager2.addIssue({
14205
+ id: number,
14206
+ url: options.issueUrl,
14207
+ repo: `${owner}/${repo}`,
14208
+ number,
14209
+ title: "(claimed)",
14210
+ status: "claimed",
14211
+ labels: [],
14212
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
14213
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14214
+ vetted: false
14215
+ });
14216
+ stateManager2.save();
14217
+ } catch (error) {
14218
+ console.error(
14219
+ `Warning: Comment posted on ${options.issueUrl} but failed to save to local state: ${error instanceof Error ? error.message : error}`
14220
+ );
14221
+ }
14211
14222
  return {
14212
14223
  commentUrl: comment.html_url,
14213
14224
  issueUrl: options.issueUrl
@@ -14320,6 +14331,13 @@ __export(setup_exports, {
14320
14331
  runCheckSetup: () => runCheckSetup,
14321
14332
  runSetup: () => runSetup
14322
14333
  });
14334
+ function parsePositiveInt(value, settingName) {
14335
+ const parsed = Number(value);
14336
+ if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
14337
+ throw new ValidationError(`Invalid value for ${settingName}: "${value}". Must be a positive integer.`);
14338
+ }
14339
+ return parsed;
14340
+ }
14323
14341
  async function runSetup(options) {
14324
14342
  const stateManager2 = getStateManager();
14325
14343
  const config = stateManager2.getState().config;
@@ -14335,18 +14353,24 @@ async function runSetup(options) {
14335
14353
  stateManager2.updateConfig({ githubUsername: value });
14336
14354
  results[key] = value;
14337
14355
  break;
14338
- case "maxActivePRs":
14339
- stateManager2.updateConfig({ maxActivePRs: parseInt(value) || 10 });
14340
- results[key] = value;
14356
+ case "maxActivePRs": {
14357
+ const maxPRs = parsePositiveInt(value, "maxActivePRs");
14358
+ stateManager2.updateConfig({ maxActivePRs: maxPRs });
14359
+ results[key] = String(maxPRs);
14341
14360
  break;
14342
- case "dormantDays":
14343
- stateManager2.updateConfig({ dormantThresholdDays: parseInt(value) || 30 });
14344
- results[key] = value;
14361
+ }
14362
+ case "dormantDays": {
14363
+ const dormant = parsePositiveInt(value, "dormantDays");
14364
+ stateManager2.updateConfig({ dormantThresholdDays: dormant });
14365
+ results[key] = String(dormant);
14345
14366
  break;
14346
- case "approachingDays":
14347
- stateManager2.updateConfig({ approachingDormantDays: parseInt(value) || 25 });
14348
- results[key] = value;
14367
+ }
14368
+ case "approachingDays": {
14369
+ const approaching = parsePositiveInt(value, "approachingDays");
14370
+ stateManager2.updateConfig({ approachingDormantDays: approaching });
14371
+ results[key] = String(approaching);
14349
14372
  break;
14373
+ }
14350
14374
  case "languages":
14351
14375
  stateManager2.updateConfig({ languages: value.split(",").map((l) => l.trim()) });
14352
14376
  results[key] = value;
@@ -14369,9 +14393,12 @@ async function runSetup(options) {
14369
14393
  }
14370
14394
  break;
14371
14395
  case "minStars": {
14372
- const parsed = parseInt(value);
14373
- stateManager2.updateConfig({ minStars: isNaN(parsed) ? 50 : parsed });
14374
- results[key] = value;
14396
+ const stars = Number(value);
14397
+ if (!Number.isFinite(stars) || !Number.isInteger(stars) || stars < 0) {
14398
+ throw new ValidationError(`Invalid value for minStars: "${value}". Must be a non-negative integer.`);
14399
+ }
14400
+ stateManager2.updateConfig({ minStars: stars });
14401
+ results[key] = String(stars);
14375
14402
  break;
14376
14403
  }
14377
14404
  case "includeDocIssues":
@@ -14495,6 +14522,7 @@ var init_setup = __esm({
14495
14522
  "src/commands/setup.ts"() {
14496
14523
  "use strict";
14497
14524
  init_core();
14525
+ init_errors();
14498
14526
  init_validation();
14499
14527
  }
14500
14528
  });
@@ -14570,7 +14598,11 @@ async function fetchDashboardData(token) {
14570
14598
  digest.autoUnshelvedPRs = [];
14571
14599
  digest.summary.totalActivePRs = prs.length - freshShelved.length;
14572
14600
  stateManager2.setLastDigest(digest);
14573
- stateManager2.save();
14601
+ try {
14602
+ stateManager2.save();
14603
+ } catch (error) {
14604
+ console.error("Warning: Failed to save dashboard digest to state:", errorMessage(error));
14605
+ }
14574
14606
  console.error(`Refreshed: ${prs.length} PRs fetched`);
14575
14607
  return { digest, commentedIssues };
14576
14608
  }
@@ -16364,6 +16396,38 @@ async function startDashboardServer(options) {
16364
16396
  sendError(res, 400, 'Missing or invalid "url" field');
16365
16397
  return;
16366
16398
  }
16399
+ try {
16400
+ validateUrl(body.url);
16401
+ validateGitHubUrl(body.url, PR_URL_PATTERN, "PR");
16402
+ } catch (err) {
16403
+ if (err instanceof ValidationError) {
16404
+ sendError(res, 400, err.message);
16405
+ } else {
16406
+ console.error("Unexpected error during URL validation:", err);
16407
+ sendError(res, 400, "Invalid URL");
16408
+ }
16409
+ return;
16410
+ }
16411
+ if (body.action === "snooze") {
16412
+ const days = body.days ?? 7;
16413
+ if (typeof days !== "number" || !Number.isFinite(days) || days <= 0) {
16414
+ sendError(res, 400, "Snooze days must be a positive finite number");
16415
+ return;
16416
+ }
16417
+ if (body.reason !== void 0) {
16418
+ try {
16419
+ validateMessage(String(body.reason));
16420
+ } catch (err) {
16421
+ if (err instanceof ValidationError) {
16422
+ sendError(res, 400, err.message);
16423
+ } else {
16424
+ console.error("Unexpected error during message validation:", err);
16425
+ sendError(res, 400, "Invalid reason");
16426
+ }
16427
+ return;
16428
+ }
16429
+ }
16430
+ }
16367
16431
  try {
16368
16432
  switch (body.action) {
16369
16433
  case "shelve":
@@ -16373,7 +16437,7 @@ async function startDashboardServer(options) {
16373
16437
  stateManager2.unshelvePR(body.url);
16374
16438
  break;
16375
16439
  case "snooze":
16376
- stateManager2.snoozePR(body.url, body.reason || "Snoozed via dashboard", body.days || 7);
16440
+ stateManager2.snoozePR(body.url, body.reason || "Snoozed via dashboard", body.days ?? 7);
16377
16441
  break;
16378
16442
  case "unsnooze":
16379
16443
  stateManager2.unsnoozePR(body.url);
@@ -16527,6 +16591,7 @@ var init_dashboard_server = __esm({
16527
16591
  path5 = __toESM(require("path"), 1);
16528
16592
  init_core();
16529
16593
  init_errors();
16594
+ init_validation();
16530
16595
  init_dashboard_data();
16531
16596
  init_dashboard_templates();
16532
16597
  VALID_ACTIONS = /* @__PURE__ */ new Set(["shelve", "unshelve", "snooze", "unsnooze"]);
@@ -16580,6 +16645,8 @@ async function runDashboard(options) {
16580
16645
  digest = stateManager2.getState().lastDigest;
16581
16646
  }
16582
16647
  } else {
16648
+ console.error("Warning: No GitHub token found. Using cached data (may be stale).");
16649
+ console.error("Set GITHUB_TOKEN or run `gh auth login` for fresh data.");
16583
16650
  digest = stateManager2.getState().lastDigest;
16584
16651
  }
16585
16652
  if (!digest) {
@@ -16618,7 +16685,6 @@ async function runDashboard(options) {
16618
16685
  const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses);
16619
16686
  const dashboardPath = getDashboardPath();
16620
16687
  fs6.writeFileSync(dashboardPath, html, { mode: 420 });
16621
- fs6.chmodSync(dashboardPath, 420);
16622
16688
  if (options.offline) {
16623
16689
  const lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
16624
16690
  console.log(`
@@ -16654,7 +16720,6 @@ function writeDashboardFromState() {
16654
16720
  const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state);
16655
16721
  const dashboardPath = getDashboardPath();
16656
16722
  fs6.writeFileSync(dashboardPath, html, { mode: 420 });
16657
- fs6.chmodSync(dashboardPath, 420);
16658
16723
  return dashboardPath;
16659
16724
  }
16660
16725
  function resolveAssetsDir() {
@@ -17043,7 +17108,7 @@ async function runLocalRepos(options) {
17043
17108
  stateManager2.save();
17044
17109
  } catch (error) {
17045
17110
  const msg = errorMessage(error);
17046
- debug("local-repos", `Failed to cache scan results: ${msg}`);
17111
+ console.error(`Warning: Failed to cache scan results: ${msg}`);
17047
17112
  }
17048
17113
  return {
17049
17114
  repos,
@@ -17424,12 +17489,20 @@ Last Run: ${data.lastRunAt || "Never"}`);
17424
17489
  program2.command("search [count]").description("Search for new issues to work on").option("--json", "Output as JSON").action(async (count, options) => {
17425
17490
  try {
17426
17491
  const { runSearch: runSearch2 } = await Promise.resolve().then(() => (init_search(), search_exports));
17492
+ let maxResults = 5;
17493
+ if (count !== void 0) {
17494
+ const parsed = Number(count);
17495
+ if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
17496
+ throw new Error(`Invalid count "${count}". Must be a positive integer.`);
17497
+ }
17498
+ maxResults = parsed;
17499
+ }
17427
17500
  if (!options.json) {
17428
17501
  console.log(`
17429
- Searching for issues (max ${parseInt(count) || 5})...
17502
+ Searching for issues (max ${maxResults})...
17430
17503
  `);
17431
17504
  }
17432
- const data = await runSearch2({ maxResults: parseInt(count) || 5 });
17505
+ const data = await runSearch2({ maxResults });
17433
17506
  if (options.json) {
17434
17507
  outputJson(data);
17435
17508
  } else {
@@ -17728,17 +17801,25 @@ program2.command("checkSetup").description("Check if setup is complete").option(
17728
17801
  });
17729
17802
  var dashboardCmd = program2.command("dashboard").description("Dashboard commands");
17730
17803
  dashboardCmd.command("serve").description("Start interactive dashboard server").option("--port <port>", "Port to listen on", "3000").option("--no-open", "Do not open browser automatically").action(async (options) => {
17731
- const port = parseInt(options.port, 10);
17732
- if (isNaN(port) || port < 1 || port > 65535) {
17733
- console.error(`Invalid port number: "${options.port}". Must be an integer between 1 and 65535.`);
17734
- process.exit(1);
17804
+ try {
17805
+ const port = parseInt(options.port, 10);
17806
+ if (isNaN(port) || port < 1 || port > 65535) {
17807
+ console.error(`Invalid port number: "${options.port}". Must be an integer between 1 and 65535.`);
17808
+ process.exit(1);
17809
+ }
17810
+ const { serveDashboard: serveDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
17811
+ await serveDashboard2({ port, open: options.open });
17812
+ } catch (err) {
17813
+ handleCommandError(err);
17735
17814
  }
17736
- const { serveDashboard: serveDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
17737
- await serveDashboard2({ port, open: options.open });
17738
17815
  });
17739
17816
  dashboardCmd.option("--open", "Open in browser").option("--json", "Output as JSON").option("--offline", "Use cached data only (no GitHub API calls)").action(async (options) => {
17740
- const { runDashboard: runDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
17741
- await runDashboard2({ open: options.open, json: options.json, offline: options.offline });
17817
+ try {
17818
+ const { runDashboard: runDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
17819
+ await runDashboard2({ open: options.open, json: options.json, offline: options.offline });
17820
+ } catch (err) {
17821
+ handleCommandError(err, options.json);
17822
+ }
17742
17823
  });
17743
17824
  program2.command("parse-issue-list <path>").description("Parse a markdown issue list into structured JSON").option("--json", "Output as JSON").action(async (filePath, options) => {
17744
17825
  try {
package/dist/cli.js CHANGED
@@ -127,10 +127,18 @@ program
127
127
  .action(async (count, options) => {
128
128
  try {
129
129
  const { runSearch } = await import('./commands/search.js');
130
+ let maxResults = 5;
131
+ if (count !== undefined) {
132
+ const parsed = Number(count);
133
+ if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
134
+ throw new Error(`Invalid count "${count}". Must be a positive integer.`);
135
+ }
136
+ maxResults = parsed;
137
+ }
130
138
  if (!options.json) {
131
- console.log(`\nSearching for issues (max ${parseInt(count) || 5})...\n`);
139
+ console.log(`\nSearching for issues (max ${maxResults})...\n`);
132
140
  }
133
- const data = await runSearch({ maxResults: parseInt(count) || 5 });
141
+ const data = await runSearch({ maxResults });
134
142
  if (options.json) {
135
143
  outputJson(data);
136
144
  }
@@ -516,13 +524,18 @@ dashboardCmd
516
524
  .option('--port <port>', 'Port to listen on', '3000')
517
525
  .option('--no-open', 'Do not open browser automatically')
518
526
  .action(async (options) => {
519
- const port = parseInt(options.port, 10);
520
- if (isNaN(port) || port < 1 || port > 65535) {
521
- console.error(`Invalid port number: "${options.port}". Must be an integer between 1 and 65535.`);
522
- process.exit(1);
527
+ try {
528
+ const port = parseInt(options.port, 10);
529
+ if (isNaN(port) || port < 1 || port > 65535) {
530
+ console.error(`Invalid port number: "${options.port}". Must be an integer between 1 and 65535.`);
531
+ process.exit(1);
532
+ }
533
+ const { serveDashboard } = await import('./commands/dashboard.js');
534
+ await serveDashboard({ port, open: options.open });
535
+ }
536
+ catch (err) {
537
+ handleCommandError(err);
523
538
  }
524
- const { serveDashboard } = await import('./commands/dashboard.js');
525
- await serveDashboard({ port, open: options.open });
526
539
  });
527
540
  // Keep bare `dashboard` (no subcommand) for backward compat — generates static HTML
528
541
  dashboardCmd
@@ -530,8 +543,13 @@ dashboardCmd
530
543
  .option('--json', 'Output as JSON')
531
544
  .option('--offline', 'Use cached data only (no GitHub API calls)')
532
545
  .action(async (options) => {
533
- const { runDashboard } = await import('./commands/dashboard.js');
534
- await runDashboard({ open: options.open, json: options.json, offline: options.offline });
546
+ try {
547
+ const { runDashboard } = await import('./commands/dashboard.js');
548
+ await runDashboard({ open: options.open, json: options.json, offline: options.offline });
549
+ }
550
+ catch (err) {
551
+ handleCommandError(err, options.json);
552
+ }
535
553
  });
536
554
  // Parse issue list command (#82)
537
555
  program
@@ -4,9 +4,10 @@
4
4
  */
5
5
  import { getStateManager, getOctokit, parseGitHubUrl, requireGitHubToken } from '../core/index.js';
6
6
  import { paginateAll } from '../core/pagination.js';
7
- import { validateUrl, validateMessage } from './validation.js';
7
+ import { validateUrl, validateMessage, validateGitHubUrl, PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, ISSUE_URL_PATTERN, } from './validation.js';
8
8
  export async function runComments(options) {
9
9
  validateUrl(options.prUrl);
10
+ validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
10
11
  const token = requireGitHubToken();
11
12
  const stateManager = getStateManager();
12
13
  const octokit = getOctokit(token);
@@ -97,6 +98,7 @@ export async function runComments(options) {
97
98
  }
98
99
  export async function runPost(options) {
99
100
  validateUrl(options.url);
101
+ validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, 'issue or PR');
100
102
  if (!options.message.trim()) {
101
103
  throw new Error('No message provided');
102
104
  }
@@ -122,6 +124,7 @@ export async function runPost(options) {
122
124
  }
123
125
  export async function runClaim(options) {
124
126
  validateUrl(options.issueUrl);
127
+ validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue');
125
128
  const token = requireGitHubToken();
126
129
  // Default claim message or custom
127
130
  const message = options.message || "Hi! I'd like to work on this issue. Could you assign it to me?";
@@ -139,21 +142,26 @@ export async function runClaim(options) {
139
142
  issue_number: number,
140
143
  body: message,
141
144
  });
142
- // Add to tracked issues
143
- const stateManager = getStateManager();
144
- stateManager.addIssue({
145
- id: number,
146
- url: options.issueUrl,
147
- repo: `${owner}/${repo}`,
148
- number,
149
- title: '(claimed)',
150
- status: 'claimed',
151
- labels: [],
152
- createdAt: new Date().toISOString(),
153
- updatedAt: new Date().toISOString(),
154
- vetted: false,
155
- });
156
- stateManager.save();
145
+ // Add to tracked issues — non-fatal if state save fails (comment already posted)
146
+ try {
147
+ const stateManager = getStateManager();
148
+ stateManager.addIssue({
149
+ id: number,
150
+ url: options.issueUrl,
151
+ repo: `${owner}/${repo}`,
152
+ number,
153
+ title: '(claimed)',
154
+ status: 'claimed',
155
+ labels: [],
156
+ createdAt: new Date().toISOString(),
157
+ updatedAt: new Date().toISOString(),
158
+ vetted: false,
159
+ });
160
+ stateManager.save();
161
+ }
162
+ catch (error) {
163
+ console.error(`Warning: Comment posted on ${options.issueUrl} but failed to save to local state: ${error instanceof Error ? error.message : error}`);
164
+ }
157
165
  return {
158
166
  commentUrl: comment.html_url,
159
167
  issueUrl: options.issueUrl,
@@ -88,7 +88,12 @@ export async function fetchDashboardData(token) {
88
88
  digest.autoUnshelvedPRs = [];
89
89
  digest.summary.totalActivePRs = prs.length - freshShelved.length;
90
90
  stateManager.setLastDigest(digest);
91
- stateManager.save();
91
+ try {
92
+ stateManager.save();
93
+ }
94
+ catch (error) {
95
+ console.error('Warning: Failed to save dashboard digest to state:', errorMessage(error));
96
+ }
92
97
  console.error(`Refreshed: ${prs.length} PRs fetched`);
93
98
  return { digest, commentedIssues };
94
99
  }
@@ -9,7 +9,8 @@ import * as http from 'http';
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import { getStateManager, getGitHubToken } from '../core/index.js';
12
- import { errorMessage } from '../core/errors.js';
12
+ import { errorMessage, ValidationError } from '../core/errors.js';
13
+ import { validateUrl, validateGitHubUrl, validateMessage, PR_URL_PATTERN } from './validation.js';
13
14
  import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData } from './dashboard-data.js';
14
15
  import { buildDashboardStats } from './dashboard-templates.js';
15
16
  // ── Constants ────────────────────────────────────────────────────────────────
@@ -185,6 +186,44 @@ export async function startDashboardServer(options) {
185
186
  sendError(res, 400, 'Missing or invalid "url" field');
186
187
  return;
187
188
  }
189
+ // Validate URL format — same checks as CLI commands
190
+ try {
191
+ validateUrl(body.url);
192
+ validateGitHubUrl(body.url, PR_URL_PATTERN, 'PR');
193
+ }
194
+ catch (err) {
195
+ if (err instanceof ValidationError) {
196
+ sendError(res, 400, err.message);
197
+ }
198
+ else {
199
+ console.error('Unexpected error during URL validation:', err);
200
+ sendError(res, 400, 'Invalid URL');
201
+ }
202
+ return;
203
+ }
204
+ // Validate snooze-specific fields
205
+ if (body.action === 'snooze') {
206
+ const days = body.days ?? 7;
207
+ if (typeof days !== 'number' || !Number.isFinite(days) || days <= 0) {
208
+ sendError(res, 400, 'Snooze days must be a positive finite number');
209
+ return;
210
+ }
211
+ if (body.reason !== undefined) {
212
+ try {
213
+ validateMessage(String(body.reason));
214
+ }
215
+ catch (err) {
216
+ if (err instanceof ValidationError) {
217
+ sendError(res, 400, err.message);
218
+ }
219
+ else {
220
+ console.error('Unexpected error during message validation:', err);
221
+ sendError(res, 400, 'Invalid reason');
222
+ }
223
+ return;
224
+ }
225
+ }
226
+ }
188
227
  try {
189
228
  switch (body.action) {
190
229
  case 'shelve':
@@ -194,7 +233,7 @@ export async function startDashboardServer(options) {
194
233
  stateManager.unshelvePR(body.url);
195
234
  break;
196
235
  case 'snooze':
197
- stateManager.snoozePR(body.url, body.reason || 'Snoozed via dashboard', body.days || 7);
236
+ stateManager.snoozePR(body.url, body.reason || 'Snoozed via dashboard', body.days ?? 7);
198
237
  break;
199
238
  case 'unsnooze':
200
239
  stateManager.unsnoozePR(body.url);
@@ -46,7 +46,9 @@ export async function runDashboard(options) {
46
46
  }
47
47
  }
48
48
  else {
49
- // No token and not offline — fall back to cached digest
49
+ // No token and not offline — fall back to cached digest with warning
50
+ console.error('Warning: No GitHub token found. Using cached data (may be stale).');
51
+ console.error('Set GITHUB_TOKEN or run `gh auth login` for fresh data.');
50
52
  digest = stateManager.getState().lastDigest;
51
53
  }
52
54
  // Check if we have a digest to display
@@ -89,7 +91,6 @@ export async function runDashboard(options) {
89
91
  // Write to file in ~/.oss-autopilot/
90
92
  const dashboardPath = getDashboardPath();
91
93
  fs.writeFileSync(dashboardPath, html, { mode: 0o644 });
92
- fs.chmodSync(dashboardPath, 0o644);
93
94
  if (options.offline) {
94
95
  const lastUpdated = digest.generatedAt || state.lastDigestAt || state.lastRunAt;
95
96
  console.log(`\n📊 Dashboard generated (offline, cached data from ${lastUpdated}): ${dashboardPath}`);
@@ -131,7 +132,6 @@ export function writeDashboardFromState() {
131
132
  const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state);
132
133
  const dashboardPath = getDashboardPath();
133
134
  fs.writeFileSync(dashboardPath, html, { mode: 0o644 });
134
- fs.chmodSync(dashboardPath, 0o644);
135
135
  return dashboardPath;
136
136
  }
137
137
  /**
@@ -117,7 +117,7 @@ export async function runLocalRepos(options) {
117
117
  }
118
118
  catch (error) {
119
119
  const msg = errorMessage(error);
120
- debug('local-repos', `Failed to cache scan results: ${msg}`);
120
+ console.error(`Warning: Failed to cache scan results: ${msg}`);
121
121
  }
122
122
  return {
123
123
  repos,
@@ -3,7 +3,16 @@
3
3
  * Interactive setup / configuration
4
4
  */
5
5
  import { getStateManager, DEFAULT_CONFIG } from '../core/index.js';
6
+ import { ValidationError } from '../core/errors.js';
6
7
  import { validateGitHubUsername } from './validation.js';
8
+ /** Parse and validate a positive integer setting value. */
9
+ function parsePositiveInt(value, settingName) {
10
+ const parsed = Number(value);
11
+ if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
12
+ throw new ValidationError(`Invalid value for ${settingName}: "${value}". Must be a positive integer.`);
13
+ }
14
+ return parsed;
15
+ }
7
16
  export async function runSetup(options) {
8
17
  const stateManager = getStateManager();
9
18
  const config = stateManager.getState().config;
@@ -20,18 +29,24 @@ export async function runSetup(options) {
20
29
  stateManager.updateConfig({ githubUsername: value });
21
30
  results[key] = value;
22
31
  break;
23
- case 'maxActivePRs':
24
- stateManager.updateConfig({ maxActivePRs: parseInt(value) || 10 });
25
- results[key] = value;
32
+ case 'maxActivePRs': {
33
+ const maxPRs = parsePositiveInt(value, 'maxActivePRs');
34
+ stateManager.updateConfig({ maxActivePRs: maxPRs });
35
+ results[key] = String(maxPRs);
26
36
  break;
27
- case 'dormantDays':
28
- stateManager.updateConfig({ dormantThresholdDays: parseInt(value) || 30 });
29
- results[key] = value;
37
+ }
38
+ case 'dormantDays': {
39
+ const dormant = parsePositiveInt(value, 'dormantDays');
40
+ stateManager.updateConfig({ dormantThresholdDays: dormant });
41
+ results[key] = String(dormant);
30
42
  break;
31
- case 'approachingDays':
32
- stateManager.updateConfig({ approachingDormantDays: parseInt(value) || 25 });
33
- results[key] = value;
43
+ }
44
+ case 'approachingDays': {
45
+ const approaching = parsePositiveInt(value, 'approachingDays');
46
+ stateManager.updateConfig({ approachingDormantDays: approaching });
47
+ results[key] = String(approaching);
34
48
  break;
49
+ }
35
50
  case 'languages':
36
51
  stateManager.updateConfig({ languages: value.split(',').map((l) => l.trim()) });
37
52
  results[key] = value;
@@ -55,9 +70,12 @@ export async function runSetup(options) {
55
70
  }
56
71
  break;
57
72
  case 'minStars': {
58
- const parsed = parseInt(value);
59
- stateManager.updateConfig({ minStars: isNaN(parsed) ? 50 : parsed });
60
- results[key] = value;
73
+ const stars = Number(value);
74
+ if (!Number.isFinite(stars) || !Number.isInteger(stars) || stars < 0) {
75
+ throw new ValidationError(`Invalid value for minStars: "${value}". Must be a non-negative integer.`);
76
+ }
77
+ stateManager.updateConfig({ minStars: stars });
78
+ results[key] = String(stars);
61
79
  break;
62
80
  }
63
81
  case 'includeDocIssues':
@@ -11,6 +11,7 @@
11
11
  * - formatBriefSummary / formatSummary / printDigest — rendering
12
12
  */
13
13
  import { formatRelativeTime } from './utils.js';
14
+ import { warn } from './logger.js';
14
15
  // ---------------------------------------------------------------------------
15
16
  // Constants
16
17
  // ---------------------------------------------------------------------------
@@ -45,7 +46,7 @@ function buildRepoMap(prs, label) {
45
46
  const repoMap = new Map();
46
47
  for (const pr of prs) {
47
48
  if (!pr.repo) {
48
- console.warn(`[${label}] Skipping PR #${pr.number} (${pr.url}) with empty repo field`);
49
+ warn(label, `Skipping PR #${pr.number} (${pr.url}) with empty repo field`);
49
50
  continue;
50
51
  }
51
52
  const existing = repoMap.get(pr.repo) || [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.42.4",
3
+ "version": "0.42.5",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {