@oss-autopilot/core 1.10.0 → 1.11.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/cli-registry.js +64 -0
- package/dist/cli.bundle.cjs +70 -68
- package/dist/cli.js +17 -0
- package/dist/commands/config.js +13 -1
- package/dist/commands/dashboard-server.js +28 -6
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +29 -1
- package/dist/commands/state-cmd.d.ts +22 -0
- package/dist/commands/state-cmd.js +64 -0
- package/dist/core/errors.d.ts +6 -0
- package/dist/core/errors.js +12 -0
- package/dist/core/gist-state-store.d.ts +12 -0
- package/dist/core/gist-state-store.js +49 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/state-schema.d.ts +29 -0
- package/dist/core/state-schema.js +4 -0
- package/dist/core/state.d.ts +5 -0
- package/dist/core/state.js +32 -3
- package/dist/core/types.d.ts +3 -2
- package/dist/core/types.js +2 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -49,6 +49,23 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
|
49
49
|
console.error('Then run your command again.');
|
|
50
50
|
process.exit(1);
|
|
51
51
|
}
|
|
52
|
+
// Activate Gist persistence if configured, before any command runs.
|
|
53
|
+
// Peek at the config file directly to avoid creating a local-only singleton
|
|
54
|
+
// (getStateManager() would lock in local mode before getStateManagerAsync runs).
|
|
55
|
+
let persistence;
|
|
56
|
+
try {
|
|
57
|
+
const { getStatePath } = await import('./core/index.js');
|
|
58
|
+
const fs = await import('fs');
|
|
59
|
+
const raw = fs.readFileSync(getStatePath(), 'utf-8');
|
|
60
|
+
persistence = JSON.parse(raw)?.config?.persistence;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// No state file or unreadable — local mode, lazy init
|
|
64
|
+
}
|
|
65
|
+
if (persistence === 'gist' && token) {
|
|
66
|
+
const { getStateManagerAsync } = await import('./core/index.js');
|
|
67
|
+
await getStateManagerAsync(token);
|
|
68
|
+
}
|
|
52
69
|
}
|
|
53
70
|
});
|
|
54
71
|
// First-run detection: if no subcommand was provided and no state file exists,
|
package/dist/commands/config.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Shows or updates configuration
|
|
4
4
|
*/
|
|
5
5
|
import { getStateManager } from '../core/index.js';
|
|
6
|
-
import { ISSUE_SCOPES } from '../core/types.js';
|
|
6
|
+
import { ISSUE_SCOPES, DIFF_TOOLS } from '../core/types.js';
|
|
7
7
|
import { validateGitHubUsername } from './validation.js';
|
|
8
8
|
function validateScope(value) {
|
|
9
9
|
if (!ISSUE_SCOPES.includes(value)) {
|
|
@@ -105,6 +105,18 @@ export async function runConfig(options) {
|
|
|
105
105
|
case 'issueListPath':
|
|
106
106
|
stateManager.updateConfig({ issueListPath: value || undefined });
|
|
107
107
|
break;
|
|
108
|
+
case 'diffTool': {
|
|
109
|
+
if (!DIFF_TOOLS.includes(value)) {
|
|
110
|
+
throw new Error(`Invalid diffTool "${value}". Valid options: ${DIFF_TOOLS.join(', ')}`);
|
|
111
|
+
}
|
|
112
|
+
stateManager.updateConfig({ diffTool: value });
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case 'diffToolCustomCommand':
|
|
116
|
+
stateManager.updateConfig({
|
|
117
|
+
diffToolCustomCommand: value || undefined,
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
108
120
|
default:
|
|
109
121
|
throw new Error(`Unknown config key: ${options.key}`);
|
|
110
122
|
}
|
|
@@ -209,8 +209,15 @@ export async function startDashboardServer(options) {
|
|
|
209
209
|
sendError(res, 429, 'Too many requests');
|
|
210
210
|
return;
|
|
211
211
|
}
|
|
212
|
-
// Re-read state
|
|
213
|
-
|
|
212
|
+
// Re-read state if modified externally (file mtime for local, Gist API for Gist mode)
|
|
213
|
+
let stateChanged = false;
|
|
214
|
+
if (stateManager.isGistMode()) {
|
|
215
|
+
stateChanged = await stateManager.refreshFromGist();
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
stateChanged = stateManager.reloadIfChanged();
|
|
219
|
+
}
|
|
220
|
+
if (stateChanged) {
|
|
214
221
|
try {
|
|
215
222
|
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
216
223
|
}
|
|
@@ -268,7 +275,12 @@ export async function startDashboardServer(options) {
|
|
|
268
275
|
// ── POST /api/action handler ─────────────────────────────────────────────
|
|
269
276
|
async function handleAction(req, res) {
|
|
270
277
|
// Reload state before mutating to avoid overwriting external CLI changes
|
|
271
|
-
stateManager.
|
|
278
|
+
if (stateManager.isGistMode()) {
|
|
279
|
+
await stateManager.refreshFromGist();
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
stateManager.reloadIfChanged();
|
|
283
|
+
}
|
|
272
284
|
let body;
|
|
273
285
|
try {
|
|
274
286
|
const raw = await readBody(req);
|
|
@@ -334,7 +346,12 @@ export async function startDashboardServer(options) {
|
|
|
334
346
|
return;
|
|
335
347
|
}
|
|
336
348
|
try {
|
|
337
|
-
stateManager.
|
|
349
|
+
if (stateManager.isGistMode()) {
|
|
350
|
+
await stateManager.refreshFromGist();
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
stateManager.reloadIfChanged();
|
|
354
|
+
}
|
|
338
355
|
warn(MODULE, 'Refreshing dashboard data from GitHub...');
|
|
339
356
|
const result = await fetchDashboardData(currentToken);
|
|
340
357
|
cachedDigest = result.digest;
|
|
@@ -449,8 +466,13 @@ export async function startDashboardServer(options) {
|
|
|
449
466
|
// so subsequent /api/data requests get live data instead of cached state.
|
|
450
467
|
if (token) {
|
|
451
468
|
fetchDashboardData(token)
|
|
452
|
-
.then((result) => {
|
|
453
|
-
stateManager.
|
|
469
|
+
.then(async (result) => {
|
|
470
|
+
if (stateManager.isGistMode()) {
|
|
471
|
+
await stateManager.refreshFromGist();
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
stateManager.reloadIfChanged();
|
|
475
|
+
}
|
|
454
476
|
cachedDigest = result.digest;
|
|
455
477
|
cachedCommentedIssues = result.commentedIssues;
|
|
456
478
|
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs);
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ export { runInit } from './init.js';
|
|
|
55
55
|
export { runSetup } from './setup.js';
|
|
56
56
|
/** Check whether setup has been completed. */
|
|
57
57
|
export { runCheckSetup } from './setup.js';
|
|
58
|
+
/** Show current persistence mode, Gist ID, and sync status. */
|
|
59
|
+
export { runStateShow } from './state-cmd.js';
|
|
60
|
+
/** Force push state to the backing Gist (no-op in local mode). */
|
|
61
|
+
export { runStateSync } from './state-cmd.js';
|
|
62
|
+
/** Disconnect from Gist persistence and switch to local-only mode. */
|
|
63
|
+
export { runStateUnlink } from './state-cmd.js';
|
|
58
64
|
/** Parse a curated markdown issue list file into structured issue items. */
|
|
59
65
|
export { runParseList, pruneIssueList } from './parse-list.js';
|
|
60
66
|
/** Check if new files are properly referenced/integrated. */
|
|
@@ -76,3 +82,4 @@ export type { InitOutput } from './init.js';
|
|
|
76
82
|
export type { ConfigSetOutput, ConfigCommandOutput } from './config.js';
|
|
77
83
|
export type { SetupSetOutput, SetupCompleteOutput, SetupRequiredOutput, SetupOutput, CheckSetupOutput, } from './setup.js';
|
|
78
84
|
export type { DailyCheckResult } from './daily.js';
|
|
85
|
+
export type { StateShowOutput, StateSyncOutput, StateUnlinkOutput } from './state-cmd.js';
|
package/dist/commands/index.js
CHANGED
|
@@ -59,6 +59,13 @@ export { runInit } from './init.js';
|
|
|
59
59
|
export { runSetup } from './setup.js';
|
|
60
60
|
/** Check whether setup has been completed. */
|
|
61
61
|
export { runCheckSetup } from './setup.js';
|
|
62
|
+
// ── State Persistence ────────────────────────────────────────────────────────
|
|
63
|
+
/** Show current persistence mode, Gist ID, and sync status. */
|
|
64
|
+
export { runStateShow } from './state-cmd.js';
|
|
65
|
+
/** Force push state to the backing Gist (no-op in local mode). */
|
|
66
|
+
export { runStateSync } from './state-cmd.js';
|
|
67
|
+
/** Disconnect from Gist persistence and switch to local-only mode. */
|
|
68
|
+
export { runStateUnlink } from './state-cmd.js';
|
|
62
69
|
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
63
70
|
/** Parse a curated markdown issue list file into structured issue items. */
|
|
64
71
|
export { runParseList, pruneIssueList } from './parse-list.js';
|
package/dist/commands/setup.d.ts
CHANGED
package/dist/commands/setup.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { getStateManager, DEFAULT_CONFIG } from '../core/index.js';
|
|
6
6
|
import { ValidationError } from '../core/errors.js';
|
|
7
7
|
import { validateGitHubUsername } from './validation.js';
|
|
8
|
-
import { PROJECT_CATEGORIES, ISSUE_SCOPES } from '../core/types.js';
|
|
8
|
+
import { PROJECT_CATEGORIES, ISSUE_SCOPES, DIFF_TOOLS, } from '../core/types.js';
|
|
9
9
|
/** Parse and validate a positive integer setting value. */
|
|
10
10
|
function parsePositiveInt(value, settingName) {
|
|
11
11
|
const parsed = Number(value);
|
|
@@ -190,10 +190,30 @@ export async function runSetup(options) {
|
|
|
190
190
|
results[key] = dedupedScopes.length > 0 ? dedupedScopes.join(', ') : '(empty — using labels only)';
|
|
191
191
|
break;
|
|
192
192
|
}
|
|
193
|
+
case 'persistence':
|
|
194
|
+
if (value !== 'local' && value !== 'gist') {
|
|
195
|
+
throw new ValidationError(`Invalid value for persistence: "${value}". Must be "local" or "gist".`);
|
|
196
|
+
}
|
|
197
|
+
stateManager.updateConfig({ persistence: value });
|
|
198
|
+
results[key] = value;
|
|
199
|
+
break;
|
|
193
200
|
case 'issueListPath':
|
|
194
201
|
stateManager.updateConfig({ issueListPath: value || undefined });
|
|
195
202
|
results[key] = value || '(cleared)';
|
|
196
203
|
break;
|
|
204
|
+
case 'diffTool': {
|
|
205
|
+
if (!DIFF_TOOLS.includes(value)) {
|
|
206
|
+
warnings.push(`Invalid diffTool "${value}". Valid: ${DIFF_TOOLS.join(', ')}`);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
stateManager.updateConfig({ diffTool: value });
|
|
210
|
+
results[key] = value;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'diffToolCustomCommand':
|
|
214
|
+
stateManager.updateConfig({ diffToolCustomCommand: value || undefined });
|
|
215
|
+
results[key] = value || '(cleared)';
|
|
216
|
+
break;
|
|
197
217
|
case 'complete':
|
|
198
218
|
if (value === 'true') {
|
|
199
219
|
stateManager.markSetupComplete();
|
|
@@ -221,6 +241,7 @@ export async function runSetup(options) {
|
|
|
221
241
|
projectCategories: config.projectCategories ?? [],
|
|
222
242
|
preferredOrgs: config.preferredOrgs ?? [],
|
|
223
243
|
scope: config.scope ?? [],
|
|
244
|
+
persistence: config.persistence ?? 'local',
|
|
224
245
|
},
|
|
225
246
|
};
|
|
226
247
|
}
|
|
@@ -298,6 +319,13 @@ export async function runSetup(options) {
|
|
|
298
319
|
default: [],
|
|
299
320
|
type: 'list',
|
|
300
321
|
},
|
|
322
|
+
{
|
|
323
|
+
setting: 'persistence',
|
|
324
|
+
prompt: 'Where should state be stored? "local" for file only, "gist" for GitHub Gist (survives device loss)',
|
|
325
|
+
current: config.persistence ?? 'local',
|
|
326
|
+
default: 'local',
|
|
327
|
+
type: 'string',
|
|
328
|
+
},
|
|
301
329
|
],
|
|
302
330
|
};
|
|
303
331
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State persistence management commands.
|
|
3
|
+
* Provides --show, --sync, and --unlink subcommands for the Gist persistence layer.
|
|
4
|
+
*/
|
|
5
|
+
export interface StateShowOutput {
|
|
6
|
+
persistence: 'local' | 'gist';
|
|
7
|
+
gistId: string | null;
|
|
8
|
+
gistDegraded: boolean;
|
|
9
|
+
lastRunAt: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
export interface StateSyncOutput {
|
|
12
|
+
pushed: boolean;
|
|
13
|
+
gistId: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface StateUnlinkOutput {
|
|
16
|
+
unlinked: boolean;
|
|
17
|
+
localStatePath: string;
|
|
18
|
+
previousGistId: string | null;
|
|
19
|
+
}
|
|
20
|
+
export declare function runStateShow(): Promise<StateShowOutput>;
|
|
21
|
+
export declare function runStateSync(): Promise<StateSyncOutput>;
|
|
22
|
+
export declare function runStateUnlink(): Promise<StateUnlinkOutput>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State persistence management commands.
|
|
3
|
+
* Provides --show, --sync, and --unlink subcommands for the Gist persistence layer.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import { getStateManager, resetStateManager } from '../core/state.js';
|
|
7
|
+
import { atomicWriteFileSync } from '../core/state-persistence.js';
|
|
8
|
+
import { getStatePath, getGistIdPath } from '../core/utils.js';
|
|
9
|
+
import { warn } from '../core/logger.js';
|
|
10
|
+
const MODULE = 'state-cmd';
|
|
11
|
+
export async function runStateShow() {
|
|
12
|
+
const sm = getStateManager();
|
|
13
|
+
const state = sm.getState();
|
|
14
|
+
return {
|
|
15
|
+
persistence: state.config.persistence ?? 'local',
|
|
16
|
+
gistId: state.gistId ?? null,
|
|
17
|
+
gistDegraded: sm.isGistDegraded(),
|
|
18
|
+
lastRunAt: state.lastRunAt,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function runStateSync() {
|
|
22
|
+
const sm = getStateManager();
|
|
23
|
+
if (!sm.isGistMode()) {
|
|
24
|
+
return { pushed: false, gistId: null };
|
|
25
|
+
}
|
|
26
|
+
const pushed = await sm.checkpoint();
|
|
27
|
+
if (!pushed) {
|
|
28
|
+
throw new Error('Failed to push state to Gist after retry. Check network connectivity and token permissions.');
|
|
29
|
+
}
|
|
30
|
+
return { pushed: true, gistId: sm.getState().gistId ?? null };
|
|
31
|
+
}
|
|
32
|
+
export async function runStateUnlink() {
|
|
33
|
+
const sm = getStateManager();
|
|
34
|
+
const state = sm.getState();
|
|
35
|
+
const previousGistId = state.gistId ?? null;
|
|
36
|
+
const statePath = getStatePath();
|
|
37
|
+
// Refresh from Gist to get the latest state before unlinking
|
|
38
|
+
if (sm.isGistMode()) {
|
|
39
|
+
await sm.refreshFromGist();
|
|
40
|
+
}
|
|
41
|
+
// Write current state to local state.json with persistence set to local
|
|
42
|
+
const localState = JSON.parse(JSON.stringify(sm.getState()));
|
|
43
|
+
delete localState.gistId;
|
|
44
|
+
localState.config.persistence = 'local';
|
|
45
|
+
atomicWriteFileSync(statePath, JSON.stringify(localState, null, 2), 0o600);
|
|
46
|
+
// Remove the gist-id file so future startups don't try to use the Gist
|
|
47
|
+
const gistIdPath = getGistIdPath();
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(gistIdPath)) {
|
|
50
|
+
fs.unlinkSync(gistIdPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
warn(MODULE, `Failed to remove gist-id file: ${err}`);
|
|
55
|
+
}
|
|
56
|
+
// Do NOT delete the remote Gist — the user may want it as a backup
|
|
57
|
+
// Reset the singleton so subsequent calls in this process use local mode
|
|
58
|
+
resetStateManager();
|
|
59
|
+
return {
|
|
60
|
+
unlinked: true,
|
|
61
|
+
localStatePath: statePath,
|
|
62
|
+
previousGistId,
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ export declare class ConfigurationError extends OssAutopilotError {
|
|
|
26
26
|
export declare class ValidationError extends OssAutopilotError {
|
|
27
27
|
constructor(message: string);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Gist API scope error (token lacks the "gist" scope).
|
|
31
|
+
*/
|
|
32
|
+
export declare class GistPermissionError extends ConfigurationError {
|
|
33
|
+
constructor(message?: string);
|
|
34
|
+
}
|
|
29
35
|
/**
|
|
30
36
|
* Extract a human-readable message from an unknown error value.
|
|
31
37
|
*/
|
package/dist/core/errors.js
CHANGED
|
@@ -36,6 +36,18 @@ export class ValidationError extends OssAutopilotError {
|
|
|
36
36
|
this.name = 'ValidationError';
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Gist API scope error (token lacks the "gist" scope).
|
|
41
|
+
*/
|
|
42
|
+
export class GistPermissionError extends ConfigurationError {
|
|
43
|
+
constructor(message) {
|
|
44
|
+
super(message ??
|
|
45
|
+
'Your GitHub token does not have Gist permissions. ' +
|
|
46
|
+
'Run `gh auth refresh -s gist` to add the required scope, ' +
|
|
47
|
+
'or create a token with the "gist" scope.');
|
|
48
|
+
this.name = 'GistPermissionError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
39
51
|
/**
|
|
40
52
|
* Extract a human-readable message from an unknown error value.
|
|
41
53
|
*/
|
|
@@ -85,6 +85,8 @@ export declare class GistStateStore {
|
|
|
85
85
|
readonly cachedFiles: Map<string, string>;
|
|
86
86
|
readonly dirtyFiles: Set<string>;
|
|
87
87
|
private readonly octokit;
|
|
88
|
+
private lastRefreshAt;
|
|
89
|
+
private static readonly REFRESH_THROTTLE_MS;
|
|
88
90
|
constructor(octokit: OctokitLike);
|
|
89
91
|
/**
|
|
90
92
|
* Bootstrap the Gist store: locate or create the backing Gist,
|
|
@@ -135,6 +137,16 @@ export declare class GistStateStore {
|
|
|
135
137
|
* Throws if the Gist ID has not been resolved yet (bootstrap not called).
|
|
136
138
|
*/
|
|
137
139
|
push(): Promise<boolean>;
|
|
140
|
+
/**
|
|
141
|
+
* Re-fetch the Gist and update the in-memory cache.
|
|
142
|
+
* Throttled to at most once per 30 seconds.
|
|
143
|
+
*/
|
|
144
|
+
refreshFromGist(): Promise<boolean>;
|
|
145
|
+
/**
|
|
146
|
+
* Preflight check: verify the token has Gist API scope.
|
|
147
|
+
* Costs one cheap API call; catches permission issues early with a clear message.
|
|
148
|
+
*/
|
|
149
|
+
private checkGistScope;
|
|
138
150
|
/**
|
|
139
151
|
* Fetch a Gist by ID, populate the in-memory cache, parse state,
|
|
140
152
|
* and write the local cache file.
|
|
@@ -19,6 +19,7 @@ import { AgentStateSchema } from './state-schema.js';
|
|
|
19
19
|
import { atomicWriteFileSync, createFreshState, migrateV1ToV2, migrateV2ToV3 } from './state-persistence.js';
|
|
20
20
|
import { getGistIdPath, getStateCachePath } from './utils.js';
|
|
21
21
|
import { debug, warn } from './logger.js';
|
|
22
|
+
import { GistPermissionError, isRateLimitError } from './errors.js';
|
|
22
23
|
const MODULE = 'gist-store';
|
|
23
24
|
/** Well-known Gist description used for search-based discovery. */
|
|
24
25
|
export const GIST_DESCRIPTION = 'oss-autopilot-state';
|
|
@@ -32,6 +33,8 @@ export class GistStateStore {
|
|
|
32
33
|
cachedFiles = new Map();
|
|
33
34
|
dirtyFiles = new Set();
|
|
34
35
|
octokit;
|
|
36
|
+
lastRefreshAt = 0;
|
|
37
|
+
static REFRESH_THROTTLE_MS = 30_000;
|
|
35
38
|
constructor(octokit) {
|
|
36
39
|
this.octokit = octokit;
|
|
37
40
|
}
|
|
@@ -41,6 +44,7 @@ export class GistStateStore {
|
|
|
41
44
|
*/
|
|
42
45
|
async bootstrap() {
|
|
43
46
|
try {
|
|
47
|
+
await this.checkGistScope();
|
|
44
48
|
// Step 1: Try loading Gist ID from local file
|
|
45
49
|
const localId = this.readLocalGistId();
|
|
46
50
|
if (localId) {
|
|
@@ -71,6 +75,9 @@ export class GistStateStore {
|
|
|
71
75
|
return { gistId: id, state, created: true };
|
|
72
76
|
}
|
|
73
77
|
catch (err) {
|
|
78
|
+
// Configuration errors (e.g. GistPermissionError) must surface, not degrade
|
|
79
|
+
if (err instanceof GistPermissionError)
|
|
80
|
+
throw err;
|
|
74
81
|
// All API paths failed — enter degraded mode
|
|
75
82
|
warn(MODULE, 'All Gist API paths failed, entering degraded mode', err);
|
|
76
83
|
// Try reading from local cache file
|
|
@@ -107,6 +114,7 @@ export class GistStateStore {
|
|
|
107
114
|
*/
|
|
108
115
|
async bootstrapWithMigration(existingState) {
|
|
109
116
|
try {
|
|
117
|
+
await this.checkGistScope();
|
|
110
118
|
// Step 1: Try loading Gist ID from local file
|
|
111
119
|
const localId = this.readLocalGistId();
|
|
112
120
|
if (localId) {
|
|
@@ -137,6 +145,9 @@ export class GistStateStore {
|
|
|
137
145
|
return { gistId: id, state, created: true, migrated: true };
|
|
138
146
|
}
|
|
139
147
|
catch (err) {
|
|
148
|
+
// Configuration errors (e.g. GistPermissionError) must surface, not degrade
|
|
149
|
+
if (err instanceof GistPermissionError)
|
|
150
|
+
throw err;
|
|
140
151
|
// All API paths failed — enter degraded mode
|
|
141
152
|
warn(MODULE, 'bootstrapWithMigration: all Gist API paths failed, entering degraded mode', err);
|
|
142
153
|
// Try reading from local cache file
|
|
@@ -265,7 +276,45 @@ export class GistStateStore {
|
|
|
265
276
|
}
|
|
266
277
|
return true;
|
|
267
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Re-fetch the Gist and update the in-memory cache.
|
|
281
|
+
* Throttled to at most once per 30 seconds.
|
|
282
|
+
*/
|
|
283
|
+
async refreshFromGist() {
|
|
284
|
+
if (!this.gistId)
|
|
285
|
+
return false;
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
if (now - this.lastRefreshAt < GistStateStore.REFRESH_THROTTLE_MS)
|
|
288
|
+
return false;
|
|
289
|
+
try {
|
|
290
|
+
await this.fetchAndCache(this.gistId);
|
|
291
|
+
this.lastRefreshAt = now;
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
warn(MODULE, `refreshFromGist failed: ${err}`);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
268
299
|
// ── Private helpers ─────────────────────────────────────────────────
|
|
300
|
+
/**
|
|
301
|
+
* Preflight check: verify the token has Gist API scope.
|
|
302
|
+
* Costs one cheap API call; catches permission issues early with a clear message.
|
|
303
|
+
*/
|
|
304
|
+
async checkGistScope() {
|
|
305
|
+
try {
|
|
306
|
+
await this.octokit.gists.list({ per_page: 1, page: 1 });
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
if (isRateLimitError(err))
|
|
310
|
+
throw err;
|
|
311
|
+
const status = err.status;
|
|
312
|
+
if (status === 401 || status === 403) {
|
|
313
|
+
throw new GistPermissionError();
|
|
314
|
+
}
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
269
318
|
/**
|
|
270
319
|
* Fetch a Gist by ID, populate the in-memory cache, parse state,
|
|
271
320
|
* and write the local cache file.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { IssueConversationMonitor } from './issue-conversation.js';
|
|
|
11
11
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
12
12
|
export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
13
13
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, stateFileExists, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
14
|
-
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, resolveErrorCode, } from './errors.js';
|
|
14
|
+
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, resolveErrorCode, } from './errors.js';
|
|
15
15
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
16
16
|
export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
|
|
17
17
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
package/dist/core/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { IssueConversationMonitor } from './issue-conversation.js';
|
|
|
11
11
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
12
12
|
export { getOctokit, checkRateLimit } from './github.js';
|
|
13
13
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, stateFileExists, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
14
|
-
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, resolveErrorCode, } from './errors.js';
|
|
14
|
+
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, resolveErrorCode, } from './errors.js';
|
|
15
15
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
16
16
|
export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
|
|
17
17
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
@@ -32,6 +32,12 @@ export declare const IssueScopeSchema: z.ZodEnum<{
|
|
|
32
32
|
beginner: "beginner";
|
|
33
33
|
intermediate: "intermediate";
|
|
34
34
|
}>;
|
|
35
|
+
export declare const DiffToolSchema: z.ZodEnum<{
|
|
36
|
+
inline: "inline";
|
|
37
|
+
custom: "custom";
|
|
38
|
+
sourcetree: "sourcetree";
|
|
39
|
+
vscode: "vscode";
|
|
40
|
+
}>;
|
|
35
41
|
export declare const RepoSignalsSchema: z.ZodObject<{
|
|
36
42
|
hasActiveMaintainers: z.ZodBoolean;
|
|
37
43
|
isResponsive: z.ZodBoolean;
|
|
@@ -179,6 +185,10 @@ export declare const StatusOverrideSchema: z.ZodObject<{
|
|
|
179
185
|
export declare const AgentConfigSchema: z.ZodObject<{
|
|
180
186
|
setupComplete: z.ZodDefault<z.ZodBoolean>;
|
|
181
187
|
setupCompletedAt: z.ZodOptional<z.ZodString>;
|
|
188
|
+
persistence: z.ZodDefault<z.ZodEnum<{
|
|
189
|
+
local: "local";
|
|
190
|
+
gist: "gist";
|
|
191
|
+
}>>;
|
|
182
192
|
maxActivePRs: z.ZodDefault<z.ZodNumber>;
|
|
183
193
|
dormantThresholdDays: z.ZodDefault<z.ZodNumber>;
|
|
184
194
|
approachingDormantDays: z.ZodDefault<z.ZodNumber>;
|
|
@@ -223,6 +233,13 @@ export declare const AgentConfigSchema: z.ZodObject<{
|
|
|
223
233
|
education: "education";
|
|
224
234
|
}>>>;
|
|
225
235
|
preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
236
|
+
diffTool: z.ZodDefault<z.ZodEnum<{
|
|
237
|
+
inline: "inline";
|
|
238
|
+
custom: "custom";
|
|
239
|
+
sourcetree: "sourcetree";
|
|
240
|
+
vscode: "vscode";
|
|
241
|
+
}>>;
|
|
242
|
+
diffToolCustomCommand: z.ZodOptional<z.ZodString>;
|
|
226
243
|
}, z.core.$strip>;
|
|
227
244
|
export declare const LocalRepoCacheSchema: z.ZodObject<{
|
|
228
245
|
repos: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -325,6 +342,10 @@ export declare const AgentStateSchema: z.ZodObject<{
|
|
|
325
342
|
config: z.ZodDefault<z.ZodObject<{
|
|
326
343
|
setupComplete: z.ZodDefault<z.ZodBoolean>;
|
|
327
344
|
setupCompletedAt: z.ZodOptional<z.ZodString>;
|
|
345
|
+
persistence: z.ZodDefault<z.ZodEnum<{
|
|
346
|
+
local: "local";
|
|
347
|
+
gist: "gist";
|
|
348
|
+
}>>;
|
|
328
349
|
maxActivePRs: z.ZodDefault<z.ZodNumber>;
|
|
329
350
|
dormantThresholdDays: z.ZodDefault<z.ZodNumber>;
|
|
330
351
|
approachingDormantDays: z.ZodDefault<z.ZodNumber>;
|
|
@@ -369,6 +390,13 @@ export declare const AgentStateSchema: z.ZodObject<{
|
|
|
369
390
|
education: "education";
|
|
370
391
|
}>>>;
|
|
371
392
|
preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
393
|
+
diffTool: z.ZodDefault<z.ZodEnum<{
|
|
394
|
+
inline: "inline";
|
|
395
|
+
custom: "custom";
|
|
396
|
+
sourcetree: "sourcetree";
|
|
397
|
+
vscode: "vscode";
|
|
398
|
+
}>>;
|
|
399
|
+
diffToolCustomCommand: z.ZodOptional<z.ZodString>;
|
|
372
400
|
}, z.core.$strip>>;
|
|
373
401
|
lastRunAt: z.ZodDefault<z.ZodString>;
|
|
374
402
|
lastDigestAt: z.ZodOptional<z.ZodString>;
|
|
@@ -499,6 +527,7 @@ export type IssueStatus = z.infer<typeof IssueStatusSchema>;
|
|
|
499
527
|
export type FetchedPRStatus = z.infer<typeof FetchedPRStatusSchema>;
|
|
500
528
|
export type ProjectCategory = z.infer<typeof ProjectCategorySchema>;
|
|
501
529
|
export type IssueScope = z.infer<typeof IssueScopeSchema>;
|
|
530
|
+
export type DiffTool = z.infer<typeof DiffToolSchema>;
|
|
502
531
|
export type RepoSignals = z.infer<typeof RepoSignalsSchema>;
|
|
503
532
|
export type RepoScore = z.infer<typeof RepoScoreSchema>;
|
|
504
533
|
export type StoredMergedPR = z.infer<typeof StoredMergedPRSchema>;
|
|
@@ -21,6 +21,7 @@ export const ProjectCategorySchema = z.enum([
|
|
|
21
21
|
'education',
|
|
22
22
|
]);
|
|
23
23
|
export const IssueScopeSchema = z.enum(['beginner', 'intermediate', 'advanced']);
|
|
24
|
+
export const DiffToolSchema = z.enum(['inline', 'sourcetree', 'vscode', 'custom']);
|
|
24
25
|
// ── 2. Leaf schemas ──────────────────────────────────────────────────
|
|
25
26
|
export const RepoSignalsSchema = z.object({
|
|
26
27
|
hasActiveMaintainers: z.boolean(),
|
|
@@ -116,6 +117,7 @@ export const StatusOverrideSchema = z.object({
|
|
|
116
117
|
export const AgentConfigSchema = z.object({
|
|
117
118
|
setupComplete: z.boolean().default(false),
|
|
118
119
|
setupCompletedAt: z.string().optional(),
|
|
120
|
+
persistence: z.enum(['local', 'gist']).default('local'),
|
|
119
121
|
maxActivePRs: z.number().default(10),
|
|
120
122
|
dormantThresholdDays: z.number().default(30),
|
|
121
123
|
approachingDormantDays: z.number().default(25),
|
|
@@ -142,6 +144,8 @@ export const AgentConfigSchema = z.object({
|
|
|
142
144
|
skippedIssuesPath: z.string().optional(),
|
|
143
145
|
projectCategories: z.array(ProjectCategorySchema).default([]),
|
|
144
146
|
preferredOrgs: z.array(z.string()).default([]),
|
|
147
|
+
diffTool: DiffToolSchema.default('inline'),
|
|
148
|
+
diffToolCustomCommand: z.string().optional(),
|
|
145
149
|
});
|
|
146
150
|
// ── 6. Cache schemas ─────────────────────────────────────────────────
|
|
147
151
|
export const LocalRepoCacheSchema = z.object({
|
package/dist/core/state.d.ts
CHANGED
|
@@ -90,6 +90,11 @@ export declare class StateManager {
|
|
|
90
90
|
* Returns true if state was reloaded, false if unchanged or in-memory mode.
|
|
91
91
|
*/
|
|
92
92
|
reloadIfChanged(): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Re-fetch state from the backing Gist (if in Gist mode).
|
|
95
|
+
* Throttled to once per 30 seconds by GistStateStore. Returns true if state was refreshed.
|
|
96
|
+
*/
|
|
97
|
+
refreshFromGist(): Promise<boolean>;
|
|
93
98
|
/**
|
|
94
99
|
* Store the latest daily digest and update the digest timestamp.
|
|
95
100
|
* @param digest - The daily digest to store
|
package/dist/core/state.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* and scoring logic to repo-score-manager.ts.
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'fs';
|
|
7
|
+
import { AgentStateSchema } from './state-schema.js';
|
|
7
8
|
import { loadState, saveState, reloadStateIfChanged, createFreshState, atomicWriteFileSync, } from './state-persistence.js';
|
|
8
9
|
import * as repoScoring from './repo-score-manager.js';
|
|
9
10
|
import { debug, warn } from './logger.js';
|
|
10
|
-
import { errorMessage } from './errors.js';
|
|
11
|
+
import { errorMessage, ConfigurationError } from './errors.js';
|
|
11
12
|
import { GistStateStore } from './gist-state-store.js';
|
|
12
13
|
import { getStatePath, getStateCachePath } from './utils.js';
|
|
13
14
|
export { acquireLock, releaseLock, atomicWriteFileSync } from './state-persistence.js';
|
|
@@ -230,6 +231,31 @@ export class StateManager {
|
|
|
230
231
|
this.tryReconcilePRCounts();
|
|
231
232
|
return true;
|
|
232
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Re-fetch state from the backing Gist (if in Gist mode).
|
|
236
|
+
* Throttled to once per 30 seconds by GistStateStore. Returns true if state was refreshed.
|
|
237
|
+
*/
|
|
238
|
+
async refreshFromGist() {
|
|
239
|
+
if (!this.gistStore)
|
|
240
|
+
return false;
|
|
241
|
+
const refreshed = await this.gistStore.refreshFromGist();
|
|
242
|
+
if (refreshed) {
|
|
243
|
+
const raw = this.gistStore.cachedFiles.get('state.json');
|
|
244
|
+
if (!raw) {
|
|
245
|
+
warn(MODULE, 'Gist refreshed but state.json missing from cache');
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
this.state = AgentStateSchema.parse(JSON.parse(raw));
|
|
250
|
+
this.tryReconcilePRCounts();
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
warn(MODULE, `Failed to parse refreshed Gist state: ${errorMessage(err)}`);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return refreshed;
|
|
258
|
+
}
|
|
233
259
|
// === Dashboard Data Setters ===
|
|
234
260
|
/**
|
|
235
261
|
* Store the latest daily digest and update the digest timestamp.
|
|
@@ -669,8 +695,11 @@ export async function getStateManagerAsync(token) {
|
|
|
669
695
|
})
|
|
670
696
|
.catch((err) => {
|
|
671
697
|
asyncManagerPromise = null;
|
|
672
|
-
|
|
673
|
-
|
|
698
|
+
// Configuration errors (e.g. GistPermissionError) must surface to the user
|
|
699
|
+
if (err instanceof ConfigurationError)
|
|
700
|
+
throw err;
|
|
701
|
+
warn(MODULE, `Gist initialization failed, falling back to local-only mode: ${err}`);
|
|
702
|
+
return getStateManager(); // fall back to sync/local for transient errors
|
|
674
703
|
});
|
|
675
704
|
return asyncManagerPromise;
|
|
676
705
|
}
|