nightshift-mcp 2.1.3 → 2.3.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/README.md +65 -3
- package/dist/config.d.ts +100 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +208 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon-manager.d.ts +24 -0
- package/dist/daemon-manager.d.ts.map +1 -1
- package/dist/daemon-manager.js +142 -0
- package/dist/daemon-manager.js.map +1 -1
- package/dist/daemon.d.ts +14 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +231 -29
- package/dist/daemon.js.map +1 -1
- package/dist/routines.d.ts +64 -0
- package/dist/routines.d.ts.map +1 -0
- package/dist/routines.js +247 -0
- package/dist/routines.js.map +1 -0
- package/dist/tools/agents.d.ts +8 -0
- package/dist/tools/agents.d.ts.map +1 -1
- package/dist/tools/agents.js +295 -1
- package/dist/tools/agents.js.map +1 -1
- package/dist/tools/prd.d.ts +2 -0
- package/dist/tools/prd.d.ts.map +1 -1
- package/dist/tools/prd.js +120 -0
- package/dist/tools/prd.js.map +1 -1
- package/dist/vision.d.ts +67 -0
- package/dist/vision.d.ts.map +1 -0
- package/dist/vision.js +305 -0
- package/dist/vision.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,12 +37,18 @@ claude codex
|
|
|
37
37
|
- **Agent spawning**: Spawn Claude, Codex, Gemini, Vibe, Goose, or Ollama as subprocesses with full lifecycle tracking
|
|
38
38
|
- **Autonomous mode**: Single `orchestrate` tool runs claim-implement-complete loops until all stories pass
|
|
39
39
|
- **Daemon mode**: Event-driven background orchestrator with file watchers, health checks, and auto-recovery
|
|
40
|
+
- **Operating modes**: Sprint (build features) → Maintain (run routines) → Grow (vision re-eval) → Sprint lifecycle
|
|
41
|
+
- **Vision-driven auto-generation**: Define a project vision; the system automatically generates new stories when work runs low
|
|
42
|
+
- **Scheduled operation**: Configure time windows (e.g., 1AM-5AM) for autonomous overnight work
|
|
43
|
+
- **Maintenance routines**: Recurring tasks (security audits, dependency updates, test coverage) that run on schedule
|
|
40
44
|
- **Delegation**: Hand off stories or research tasks to specific agents with full context injection
|
|
41
45
|
- **Failover handling**: Seamless handoffs when an agent hits rate limits or context windows
|
|
42
46
|
- **Runtime project switching**: Switch active project without restarting the MCP server
|
|
43
47
|
|
|
44
48
|
### Reliability
|
|
45
49
|
- **Health tracker**: Per-agent failure tracking. Auto-disables failing agents, re-enables after configurable cooldown. Persisted across daemon restarts.
|
|
50
|
+
- **Exponential backoff**: When all agents hit rate limits, global pause with exponential backoff (5min → 10min → 20min...) instead of burning through retries
|
|
51
|
+
- **Dedup protection**: Completed stories are tracked per session to prevent duplicate verification and redundant commits
|
|
46
52
|
- **Immutable audit trail**: Append-only SQLite table records every spawn, completion, failure, and stuck-agent kill
|
|
47
53
|
- **Run records**: Structured execution history per agent run — duration, exit status, files changed, scope leak detection
|
|
48
54
|
- **Budget tracking**: Per-agent cost tracking with daily/monthly limits, warning thresholds, enable/disable toggle
|
|
@@ -70,7 +76,7 @@ claude codex
|
|
|
70
76
|
### Platform
|
|
71
77
|
- **Cross-platform**: Windows, Linux, macOS
|
|
72
78
|
- **6 agent types**: Claude, Codex, Gemini, Vibe, Goose, Ollama
|
|
73
|
-
- **
|
|
79
|
+
- **62 MCP tools** across 10 categories
|
|
74
80
|
- **NightShift CLI**: `nightshift` command for agent coordination without MCP
|
|
75
81
|
- **Zero external services**: Everything runs locally with SQLite + file storage
|
|
76
82
|
|
|
@@ -144,6 +150,41 @@ orchestrate()
|
|
|
144
150
|
nightshift-daemon --verbose --max-agents 4
|
|
145
151
|
```
|
|
146
152
|
|
|
153
|
+
### Scheduled overnight operation
|
|
154
|
+
```
|
|
155
|
+
set_schedule({ start: "01:00", stop: "05:00", timezone: "America/Chicago", days: ["mon","tue","wed","thu","fri"] })
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Vision-driven (auto-generate stories)
|
|
159
|
+
```json
|
|
160
|
+
// nightshift.config.json
|
|
161
|
+
{
|
|
162
|
+
"vision": "Your project's end-state goal goes here. Be specific about what 'done' looks like.",
|
|
163
|
+
"mode": "auto",
|
|
164
|
+
"sprint": { "autoGenerateWhenBelow": 5 },
|
|
165
|
+
"maintain": {
|
|
166
|
+
"routines": [
|
|
167
|
+
{ "name": "security-audit", "schedule": "weekly", "agent": "gemini", "type": "audit",
|
|
168
|
+
"prompt": "Audit the codebase for security vulnerabilities and file bugs for findings." },
|
|
169
|
+
{ "name": "test-coverage", "schedule": "daily", "agent": "claude", "type": "quality",
|
|
170
|
+
"prompt": "Find untested code paths. Write tests for the 3 most critical gaps." }
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
"grow": {
|
|
174
|
+
"strategy": "Identify growth opportunities based on the project vision",
|
|
175
|
+
"cadence": "weekly",
|
|
176
|
+
"maxStoriesPerCycle": 5,
|
|
177
|
+
"agent": "gemini"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
When `mode` is `"auto"`, the daemon transitions through the full lifecycle:
|
|
183
|
+
|
|
184
|
+
1. **Sprint**: Execute stories from `prd.json` until all complete. When stories run low and a vision is set, auto-generates more.
|
|
185
|
+
2. **Maintain**: Run recurring routines (security, dependencies, tests). If routines file new bugs or stories, transitions back to sprint.
|
|
186
|
+
3. **Grow**: Re-evaluates the vision against the codebase and generates strategic stories for the next sprint.
|
|
187
|
+
|
|
147
188
|
## PRD Setup
|
|
148
189
|
|
|
149
190
|
Create `prd.json` in your project root:
|
|
@@ -209,7 +250,7 @@ When all stories pass and all bugs are fixed, NightShift posts `READY_TO_TEST` t
|
|
|
209
250
|
|
|
210
251
|
## Tools Reference
|
|
211
252
|
|
|
212
|
-
NightShift provides
|
|
253
|
+
NightShift provides 62 tools across 10 categories. Key tools listed below — use `nightshift_setup(showExamples: true)` or `nightshift_help` for full documentation.
|
|
213
254
|
|
|
214
255
|
### Communication
|
|
215
256
|
| Tool | Description |
|
|
@@ -228,6 +269,8 @@ NightShift provides 52 tools across 10 categories. Key tools listed below — us
|
|
|
228
269
|
| `get_next_story` | Get highest priority incomplete story |
|
|
229
270
|
| `claim_story` | Claim a story and notify via chat |
|
|
230
271
|
| `complete_story` | Mark done, log progress, create savepoint |
|
|
272
|
+
| `generate_stories` | Generate new stories from project vision |
|
|
273
|
+
| `approve_stories` | Approve auto-generated stories for execution |
|
|
231
274
|
| `read_bugs` / `claim_bug` / `mark_bug_fixed` | Bug lifecycle |
|
|
232
275
|
|
|
233
276
|
### Agent Spawning
|
|
@@ -250,6 +293,18 @@ NightShift provides 52 tools across 10 categories. Key tools listed below — us
|
|
|
250
293
|
| `get_pipeline` | View configured pipelines and tag routing |
|
|
251
294
|
| `set_pipeline` | Create/update agent pipelines at runtime |
|
|
252
295
|
|
|
296
|
+
### Schedule & Modes
|
|
297
|
+
| Tool | Description |
|
|
298
|
+
|------|-------------|
|
|
299
|
+
| `set_schedule` | Set daily start/stop time window for the daemon |
|
|
300
|
+
| `get_schedule` | View current schedule |
|
|
301
|
+
| `remove_schedule` | Remove schedule |
|
|
302
|
+
| `set_mode` | Set operating mode (sprint/maintain/grow/auto) |
|
|
303
|
+
| `get_mode` | View mode and project lifecycle state |
|
|
304
|
+
| `list_routines` | View maintenance routines and their status |
|
|
305
|
+
| `add_routine` | Add a maintenance routine (built-in or custom) |
|
|
306
|
+
| `reset_routine` | Re-enable a circuit-broken routine |
|
|
307
|
+
|
|
253
308
|
### Workflow
|
|
254
309
|
| Tool | Description |
|
|
255
310
|
|------|-------------|
|
|
@@ -294,10 +349,14 @@ nightshift-daemon [options]
|
|
|
294
349
|
**What it does:**
|
|
295
350
|
- Watches `prd.json` and `chat.txt` for changes
|
|
296
351
|
- Auto-spawns agents for unassigned stories
|
|
297
|
-
-
|
|
352
|
+
- Deduplicates — won't re-verify stories already completed this session
|
|
353
|
+
- Health tracker disables failing agents, re-enables after 5-minute cooldown
|
|
354
|
+
- Exponential backoff when all agents are rate-limited (5min → 10min → 20min, max ~60min)
|
|
298
355
|
- Detects and kills stuck agents
|
|
299
356
|
- Claims failover requests
|
|
300
357
|
- Quarantines stories that fail repeatedly
|
|
358
|
+
- Runs maintenance routines in maintain mode
|
|
359
|
+
- Auto-generates stories from vision when work runs low
|
|
301
360
|
- Logs everything to the SQLite audit trail
|
|
302
361
|
|
|
303
362
|
## Local Models
|
|
@@ -346,6 +405,9 @@ NightShift uses a hybrid storage approach:
|
|
|
346
405
|
| **Health tracker** | `.robot-chat/nightshift.db` | SQLite |
|
|
347
406
|
| **Run log** | `.robot-chat/runs.jsonl` | JSONL (append-only) |
|
|
348
407
|
| **Pipeline config** | `.robot-chat/pipeline-override.json` | JSON (runtime) |
|
|
408
|
+
| **Routine state** | `.robot-chat/routine-state.json` | JSON |
|
|
409
|
+
| **Routine log** | `.robot-chat/routine-runs.jsonl` | JSONL (append-only) |
|
|
410
|
+
| **Project config** | `nightshift.config.json` | JSON (vision, mode, schedule, routines) |
|
|
349
411
|
|
|
350
412
|
Add `.robot-chat/` to your `.gitignore` — coordination state is ephemeral.
|
|
351
413
|
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NightShift Configuration
|
|
3
|
+
*
|
|
4
|
+
* Unified config loading for nightshift.config.json.
|
|
5
|
+
* All modules read from here instead of parsing the config file independently.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentType } from "./agent-spawner.js";
|
|
8
|
+
export type OperatingMode = "sprint" | "maintain" | "grow" | "auto";
|
|
9
|
+
export type RoutineSchedule = "hourly" | "daily" | "weekly" | "monthly";
|
|
10
|
+
export type RoutineType = "audit" | "maintenance" | "quality" | "optimization" | "monitoring" | "custom";
|
|
11
|
+
export interface RoutineConfig {
|
|
12
|
+
/** Unique name for this routine */
|
|
13
|
+
name: string;
|
|
14
|
+
/** How often to run */
|
|
15
|
+
schedule: RoutineSchedule;
|
|
16
|
+
/** Agent to execute the routine */
|
|
17
|
+
agent: AgentType;
|
|
18
|
+
/** Prompt describing what the routine should do */
|
|
19
|
+
prompt: string;
|
|
20
|
+
/** Category of routine */
|
|
21
|
+
type: RoutineType;
|
|
22
|
+
/** Whether this routine is enabled (default: true) */
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/** Timeout in seconds (default: 600) */
|
|
25
|
+
timeout?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ScheduleConfig {
|
|
28
|
+
/** Time to start the daemon (HH:MM, 24h format) */
|
|
29
|
+
start: string;
|
|
30
|
+
/** Time to stop the daemon (HH:MM, 24h format) */
|
|
31
|
+
stop: string;
|
|
32
|
+
/** Timezone (IANA format, e.g. "America/Chicago"). Default: system timezone */
|
|
33
|
+
timezone?: string;
|
|
34
|
+
/** Days of the week to run. Default: every day */
|
|
35
|
+
days?: ("mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun")[];
|
|
36
|
+
}
|
|
37
|
+
export interface SprintConfig {
|
|
38
|
+
/** Generate new stories when incomplete count drops below this (default: 5) */
|
|
39
|
+
autoGenerateWhenBelow: number;
|
|
40
|
+
/** Agent to use for story generation (default: "gemini") */
|
|
41
|
+
taskGenerationAgent: AgentType;
|
|
42
|
+
/** Max stories to generate per cycle (default: 10) */
|
|
43
|
+
maxAutoGenerated: number;
|
|
44
|
+
/** Require human approval before generated stories are activated (default: false) */
|
|
45
|
+
requireApproval: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface MaintainConfig {
|
|
48
|
+
/** Recurring maintenance routines */
|
|
49
|
+
routines: RoutineConfig[];
|
|
50
|
+
}
|
|
51
|
+
export interface GrowConfig {
|
|
52
|
+
/** Strategic growth evaluation prompt */
|
|
53
|
+
strategy: string;
|
|
54
|
+
/** How often to run growth evaluation */
|
|
55
|
+
cadence: RoutineSchedule;
|
|
56
|
+
/** Max stories to generate per growth cycle (default: 5) */
|
|
57
|
+
maxStoriesPerCycle: number;
|
|
58
|
+
/** Agent for growth evaluation (default: "gemini") */
|
|
59
|
+
agent: AgentType;
|
|
60
|
+
}
|
|
61
|
+
export interface NightShiftConfig {
|
|
62
|
+
/** Project vision / end-state goal */
|
|
63
|
+
vision?: string;
|
|
64
|
+
/** Operating mode */
|
|
65
|
+
mode: OperatingMode;
|
|
66
|
+
/** Sprint mode settings */
|
|
67
|
+
sprint: SprintConfig;
|
|
68
|
+
/** Maintain mode settings */
|
|
69
|
+
maintain: MaintainConfig;
|
|
70
|
+
/** Grow mode settings */
|
|
71
|
+
grow?: GrowConfig;
|
|
72
|
+
/** Daemon schedule (time window) */
|
|
73
|
+
schedule?: ScheduleConfig;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Load the full NightShift config from nightshift.config.json.
|
|
77
|
+
* Returns defaults merged with file config.
|
|
78
|
+
*/
|
|
79
|
+
export declare function loadConfig(projectPath: string): NightShiftConfig;
|
|
80
|
+
/**
|
|
81
|
+
* Save config to nightshift.config.json.
|
|
82
|
+
* Preserves any extra fields the user has (pipeline config, etc).
|
|
83
|
+
*/
|
|
84
|
+
export declare function saveConfig(projectPath: string, updates: Partial<NightShiftConfig>): void;
|
|
85
|
+
/**
|
|
86
|
+
* Get the list of default routines that users can opt into.
|
|
87
|
+
*/
|
|
88
|
+
export declare function getDefaultRoutines(): RoutineConfig[];
|
|
89
|
+
/**
|
|
90
|
+
* Parse a time string like "01:00" into { hour, minute }.
|
|
91
|
+
*/
|
|
92
|
+
export declare function parseTime(time: string): {
|
|
93
|
+
hour: number;
|
|
94
|
+
minute: number;
|
|
95
|
+
} | null;
|
|
96
|
+
/**
|
|
97
|
+
* Convert day names to cron day-of-week numbers (0=Sun, 1=Mon, ..., 6=Sat).
|
|
98
|
+
*/
|
|
99
|
+
export declare function daysToCron(days?: ScheduleConfig["days"]): string;
|
|
100
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NightShift Configuration
|
|
3
|
+
*
|
|
4
|
+
* Unified config loading for nightshift.config.json.
|
|
5
|
+
* All modules read from here instead of parsing the config file independently.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
// ============================================
|
|
10
|
+
// Defaults
|
|
11
|
+
// ============================================
|
|
12
|
+
const DEFAULT_ROUTINES = [
|
|
13
|
+
{
|
|
14
|
+
name: "security-audit",
|
|
15
|
+
schedule: "weekly",
|
|
16
|
+
agent: "gemini",
|
|
17
|
+
prompt: `Audit the codebase for security vulnerabilities:
|
|
18
|
+
1. Check for OWASP top 10 issues (injection, XSS, auth flaws, etc.)
|
|
19
|
+
2. Review dependency versions for known CVEs (check package.json / requirements.txt / go.mod)
|
|
20
|
+
3. Look for hardcoded secrets, credentials, or API keys
|
|
21
|
+
4. Check file permissions and access controls
|
|
22
|
+
|
|
23
|
+
For each finding, use the nightshift CLI to file a bug:
|
|
24
|
+
nightshift bug-claim BUG-XXX (or create a new one if needed)
|
|
25
|
+
|
|
26
|
+
Output a summary of findings and any bugs filed.`,
|
|
27
|
+
type: "audit",
|
|
28
|
+
timeout: 600,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "dependency-updates",
|
|
32
|
+
schedule: "weekly",
|
|
33
|
+
agent: "codex",
|
|
34
|
+
prompt: `Check for outdated dependencies and update safe ones:
|
|
35
|
+
1. Run the appropriate update check command (npm outdated, pip list --outdated, etc.)
|
|
36
|
+
2. Update patch and minor versions that are backward-compatible
|
|
37
|
+
3. Run tests after updates to verify nothing breaks
|
|
38
|
+
4. For major version bumps, do NOT update — instead file a story via nightshift CLI
|
|
39
|
+
|
|
40
|
+
Commit any successful updates. Report what was updated and what needs manual attention.`,
|
|
41
|
+
type: "maintenance",
|
|
42
|
+
timeout: 600,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "test-coverage",
|
|
46
|
+
schedule: "daily",
|
|
47
|
+
agent: "claude",
|
|
48
|
+
prompt: `Identify gaps in test coverage and write tests:
|
|
49
|
+
1. Look for untested files, functions, and code paths
|
|
50
|
+
2. Focus on critical business logic and error handling paths
|
|
51
|
+
3. Write 3-5 new tests for the most important gaps
|
|
52
|
+
4. Run the test suite to verify all tests pass
|
|
53
|
+
|
|
54
|
+
Use nightshift CLI to report progress. Commit passing tests.`,
|
|
55
|
+
type: "quality",
|
|
56
|
+
timeout: 600,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
function getDefaultConfig() {
|
|
60
|
+
return {
|
|
61
|
+
mode: "auto",
|
|
62
|
+
sprint: {
|
|
63
|
+
autoGenerateWhenBelow: 5,
|
|
64
|
+
taskGenerationAgent: "gemini",
|
|
65
|
+
maxAutoGenerated: 10,
|
|
66
|
+
requireApproval: false,
|
|
67
|
+
},
|
|
68
|
+
maintain: {
|
|
69
|
+
routines: [], // Empty by default — user opts in
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ============================================
|
|
74
|
+
// Config Loading
|
|
75
|
+
// ============================================
|
|
76
|
+
const CONFIG_FILENAME = "nightshift.config.json";
|
|
77
|
+
/**
|
|
78
|
+
* Load the full NightShift config from nightshift.config.json.
|
|
79
|
+
* Returns defaults merged with file config.
|
|
80
|
+
*/
|
|
81
|
+
export function loadConfig(projectPath) {
|
|
82
|
+
const config = getDefaultConfig();
|
|
83
|
+
const configFile = path.join(projectPath, CONFIG_FILENAME);
|
|
84
|
+
if (!fs.existsSync(configFile))
|
|
85
|
+
return config;
|
|
86
|
+
try {
|
|
87
|
+
const raw = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
88
|
+
// Vision
|
|
89
|
+
if (typeof raw.vision === "string") {
|
|
90
|
+
config.vision = raw.vision;
|
|
91
|
+
}
|
|
92
|
+
// Mode
|
|
93
|
+
if (raw.mode && ["sprint", "maintain", "grow", "auto"].includes(raw.mode)) {
|
|
94
|
+
config.mode = raw.mode;
|
|
95
|
+
}
|
|
96
|
+
// Sprint config
|
|
97
|
+
if (raw.sprint && typeof raw.sprint === "object") {
|
|
98
|
+
Object.assign(config.sprint, raw.sprint);
|
|
99
|
+
}
|
|
100
|
+
// Legacy flat fields (from vision.ts v1)
|
|
101
|
+
if (raw.autoGenerateWhenBelow !== undefined) {
|
|
102
|
+
config.sprint.autoGenerateWhenBelow = raw.autoGenerateWhenBelow;
|
|
103
|
+
}
|
|
104
|
+
if (raw.taskGenerationAgent !== undefined) {
|
|
105
|
+
config.sprint.taskGenerationAgent = raw.taskGenerationAgent;
|
|
106
|
+
}
|
|
107
|
+
if (raw.maxAutoGenerated !== undefined) {
|
|
108
|
+
config.sprint.maxAutoGenerated = raw.maxAutoGenerated;
|
|
109
|
+
}
|
|
110
|
+
if (raw.requireApproval !== undefined) {
|
|
111
|
+
config.sprint.requireApproval = raw.requireApproval;
|
|
112
|
+
}
|
|
113
|
+
// Maintain config
|
|
114
|
+
if (raw.maintain && typeof raw.maintain === "object") {
|
|
115
|
+
if (Array.isArray(raw.maintain.routines)) {
|
|
116
|
+
config.maintain.routines = raw.maintain.routines;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Grow config
|
|
120
|
+
if (raw.grow && typeof raw.grow === "object") {
|
|
121
|
+
config.grow = {
|
|
122
|
+
strategy: raw.grow.strategy || "Identify growth opportunities based on the project vision.",
|
|
123
|
+
cadence: raw.grow.cadence || "weekly",
|
|
124
|
+
maxStoriesPerCycle: raw.grow.maxStoriesPerCycle ?? 5,
|
|
125
|
+
agent: raw.grow.agent || "gemini",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Schedule
|
|
129
|
+
if (raw.schedule && typeof raw.schedule === "object") {
|
|
130
|
+
if (raw.schedule.start && raw.schedule.stop) {
|
|
131
|
+
config.schedule = {
|
|
132
|
+
start: raw.schedule.start,
|
|
133
|
+
stop: raw.schedule.stop,
|
|
134
|
+
timezone: raw.schedule.timezone,
|
|
135
|
+
days: raw.schedule.days,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Bad config file — use defaults
|
|
142
|
+
}
|
|
143
|
+
return config;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Save config to nightshift.config.json.
|
|
147
|
+
* Preserves any extra fields the user has (pipeline config, etc).
|
|
148
|
+
*/
|
|
149
|
+
export function saveConfig(projectPath, updates) {
|
|
150
|
+
const configFile = path.join(projectPath, CONFIG_FILENAME);
|
|
151
|
+
let existing = {};
|
|
152
|
+
if (fs.existsSync(configFile)) {
|
|
153
|
+
try {
|
|
154
|
+
existing = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Bad file — overwrite
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Deep-merge updates into existing (preserves sibling fields within objects)
|
|
161
|
+
if (updates.vision !== undefined)
|
|
162
|
+
existing.vision = updates.vision;
|
|
163
|
+
if (updates.mode !== undefined)
|
|
164
|
+
existing.mode = updates.mode;
|
|
165
|
+
if (updates.sprint !== undefined) {
|
|
166
|
+
existing.sprint = { ...(existing.sprint ?? {}), ...updates.sprint };
|
|
167
|
+
}
|
|
168
|
+
if (updates.maintain !== undefined) {
|
|
169
|
+
existing.maintain = { ...(existing.maintain ?? {}), ...updates.maintain };
|
|
170
|
+
}
|
|
171
|
+
if (updates.grow !== undefined) {
|
|
172
|
+
existing.grow = { ...(existing.grow ?? {}), ...updates.grow };
|
|
173
|
+
}
|
|
174
|
+
if (updates.schedule !== undefined)
|
|
175
|
+
existing.schedule = updates.schedule;
|
|
176
|
+
fs.writeFileSync(configFile, JSON.stringify(existing, null, 2), "utf-8");
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get the list of default routines that users can opt into.
|
|
180
|
+
*/
|
|
181
|
+
export function getDefaultRoutines() {
|
|
182
|
+
return JSON.parse(JSON.stringify(DEFAULT_ROUTINES));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Parse a time string like "01:00" into { hour, minute }.
|
|
186
|
+
*/
|
|
187
|
+
export function parseTime(time) {
|
|
188
|
+
const match = time.match(/^(\d{1,2}):(\d{2})$/);
|
|
189
|
+
if (!match)
|
|
190
|
+
return null;
|
|
191
|
+
const hour = parseInt(match[1], 10);
|
|
192
|
+
const minute = parseInt(match[2], 10);
|
|
193
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
|
|
194
|
+
return null;
|
|
195
|
+
return { hour, minute };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Convert day names to cron day-of-week numbers (0=Sun, 1=Mon, ..., 6=Sat).
|
|
199
|
+
*/
|
|
200
|
+
export function daysToCron(days) {
|
|
201
|
+
if (!days || days.length === 0 || days.length === 7)
|
|
202
|
+
return "*";
|
|
203
|
+
const dayMap = {
|
|
204
|
+
sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6,
|
|
205
|
+
};
|
|
206
|
+
return days.map(d => dayMap[d]).sort().join(",");
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAyF7B,+CAA+C;AAC/C,WAAW;AACX,+CAA+C;AAE/C,MAAM,gBAAgB,GAAoB;IACxC;QACE,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,QAAQ;QAClB,KAAK,EAAE,QAAqB;QAC5B,MAAM,EAAE;;;;;;;;;iDASqC;QAC7C,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,GAAG;KACb;IACD;QACE,IAAI,EAAE,oBAAoB;QAC1B,QAAQ,EAAE,QAAQ;QAClB,KAAK,EAAE,OAAoB;QAC3B,MAAM,EAAE;;;;;;wFAM4E;QACpF,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,GAAG;KACb;IACD;QACE,IAAI,EAAE,eAAe;QACrB,QAAQ,EAAE,OAAO;QACjB,KAAK,EAAE,QAAqB;QAC5B,MAAM,EAAE;;;;;;6DAMiD;QACzD,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,GAAG;KACb;CACF,CAAC;AAEF,SAAS,gBAAgB;IACvB,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,MAAM,EAAE;YACN,qBAAqB,EAAE,CAAC;YACxB,mBAAmB,EAAE,QAAqB;YAC1C,gBAAgB,EAAE,EAAE;YACpB,eAAe,EAAE,KAAK;SACvB;QACD,QAAQ,EAAE;YACR,QAAQ,EAAE,EAAE,EAAG,kCAAkC;SAClD;KACF,CAAC;AACJ,CAAC;AAED,+CAA+C;AAC/C,iBAAiB;AACjB,+CAA+C;AAE/C,MAAM,eAAe,GAAG,wBAAwB,CAAC;AAEjD;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,WAAmB;IAC5C,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;IAE3D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,MAAM,CAAC;IAE9C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;QAE7D,SAAS;QACT,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACnC,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QAC7B,CAAC;QAED,OAAO;QACP,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1E,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QACzB,CAAC;QAED,gBAAgB;QAChB,IAAI,GAAG,CAAC,MAAM,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACjD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3C,CAAC;QACD,yCAAyC;QACzC,IAAI,GAAG,CAAC,qBAAqB,KAAK,SAAS,EAAE,CAAC;YAC5C,MAAM,CAAC,MAAM,CAAC,qBAAqB,GAAG,GAAG,CAAC,qBAAqB,CAAC;QAClE,CAAC;QACD,IAAI,GAAG,CAAC,mBAAmB,KAAK,SAAS,EAAE,CAAC;YAC1C,MAAM,CAAC,MAAM,CAAC,mBAAmB,GAAG,GAAG,CAAC,mBAAmB,CAAC;QAC9D,CAAC;QACD,IAAI,GAAG,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACvC,MAAM,CAAC,MAAM,CAAC,gBAAgB,GAAG,GAAG,CAAC,gBAAgB,CAAC;QACxD,CAAC;QACD,IAAI,GAAG,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;YACtC,MAAM,CAAC,MAAM,CAAC,eAAe,GAAG,GAAG,CAAC,eAAe,CAAC;QACtD,CAAC;QAED,kBAAkB;QAClB,IAAI,GAAG,CAAC,QAAQ,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACrD,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACzC,MAAM,CAAC,QAAQ,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACnD,CAAC;QACH,CAAC;QAED,cAAc;QACd,IAAI,GAAG,CAAC,IAAI,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7C,MAAM,CAAC,IAAI,GAAG;gBACZ,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,4DAA4D;gBAC3F,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,QAAQ;gBACrC,kBAAkB,EAAE,GAAG,CAAC,IAAI,CAAC,kBAAkB,IAAI,CAAC;gBACpD,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,QAAQ;aAClC,CAAC;QACJ,CAAC;QAED,WAAW;QACX,IAAI,GAAG,CAAC,QAAQ,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACrD,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;gBAC5C,MAAM,CAAC,QAAQ,GAAG;oBAChB,KAAK,EAAE,GAAG,CAAC,QAAQ,CAAC,KAAK;oBACzB,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI;oBACvB,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,QAAQ;oBAC/B,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI;iBACxB,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,iCAAiC;IACnC,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,WAAmB,EAAE,OAAkC;IAChF,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;IAE3D,IAAI,QAAQ,GAA4B,EAAE,CAAC;IAC3C,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACP,uBAAuB;QACzB,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;QAAE,QAAQ,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IACnE,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS;QAAE,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC7D,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,QAAQ,CAAC,MAAM,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,MAAgB,IAAI,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAChF,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,QAAQ,CAAC,QAAQ,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,QAAkB,IAAI,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IACtF,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC/B,QAAQ,CAAC,IAAI,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAc,IAAI,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAC1E,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS;QAAE,QAAQ,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAEzE,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AAC3E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC,CAAC;AACtD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAChD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEtC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,EAAE,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,EAAE;QAAE,OAAO,IAAI,CAAC;IAEpE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,IAA6B;IACtD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAEhE,MAAM,MAAM,GAA2B;QACrC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC;KACvD,CAAC;IAEF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACnD,CAAC"}
|
package/dist/daemon-manager.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Utilities for checking daemon status and starting the daemon
|
|
5
5
|
* as a detached background process.
|
|
6
6
|
*/
|
|
7
|
+
import { type ScheduleConfig } from "./config.js";
|
|
7
8
|
export interface DaemonStatus {
|
|
8
9
|
running: boolean;
|
|
9
10
|
pid?: number;
|
|
@@ -74,4 +75,27 @@ export declare function removeWatchdog(projectPath: string): {
|
|
|
74
75
|
success: boolean;
|
|
75
76
|
error?: string;
|
|
76
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* Install start/stop cron entries for time-window daemon operation.
|
|
80
|
+
*
|
|
81
|
+
* Example: start at 01:00, stop at 05:00 on weekdays
|
|
82
|
+
* - Creates a cron entry that starts the daemon at the start time
|
|
83
|
+
* - Creates a cron entry that stops the daemon at the stop time
|
|
84
|
+
*/
|
|
85
|
+
export declare function installSchedule(projectPath: string, schedule: ScheduleConfig): {
|
|
86
|
+
success: boolean;
|
|
87
|
+
error?: string;
|
|
88
|
+
details?: string;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Remove scheduled start/stop cron entries for a project.
|
|
92
|
+
*/
|
|
93
|
+
export declare function removeSchedule(projectPath: string): {
|
|
94
|
+
success: boolean;
|
|
95
|
+
error?: string;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Get the current schedule for a project.
|
|
99
|
+
*/
|
|
100
|
+
export declare function getSchedule(projectPath: string): ScheduleConfig | null;
|
|
77
101
|
//# sourceMappingURL=daemon-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon-manager.d.ts","sourceRoot":"","sources":["../src/daemon-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"daemon-manager.d.ts","sourceRoot":"","sources":["../src/daemon-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,EAAiD,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAEjG,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,CAgDjE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAY7D;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+LzF;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA6CpF;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAM,GACpD,OAAO,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAgCD;AAqDD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAsDzF;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAoCxF;AAeD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,cAAc,GACvB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CA2FxD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAwCxF;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAGtE"}
|
package/dist/daemon-manager.js
CHANGED
|
@@ -9,6 +9,7 @@ import * as path from "path";
|
|
|
9
9
|
import { spawnSync, execSync } from "child_process";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
11
|
import { isProcessRunning, isWindows, killProcess } from "./platform.js";
|
|
12
|
+
import { loadConfig, saveConfig, parseTime, daysToCron } from "./config.js";
|
|
12
13
|
/**
|
|
13
14
|
* Get the daemon lock file path for a project
|
|
14
15
|
*/
|
|
@@ -478,4 +479,145 @@ export function removeWatchdog(projectPath) {
|
|
|
478
479
|
};
|
|
479
480
|
}
|
|
480
481
|
}
|
|
482
|
+
// ============================================
|
|
483
|
+
// Daemon Schedule (Time Window)
|
|
484
|
+
// ============================================
|
|
485
|
+
const SCHEDULE_START_TAG = "nightshift-start";
|
|
486
|
+
const SCHEDULE_STOP_TAG = "nightshift-stop";
|
|
487
|
+
function getScheduleTag(projectPath, type) {
|
|
488
|
+
const hash = projectPath.replace(/[^a-zA-Z0-9]/g, "").slice(-20);
|
|
489
|
+
const base = type === "start" ? SCHEDULE_START_TAG : SCHEDULE_STOP_TAG;
|
|
490
|
+
return `${base}-${hash}`;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Install start/stop cron entries for time-window daemon operation.
|
|
494
|
+
*
|
|
495
|
+
* Example: start at 01:00, stop at 05:00 on weekdays
|
|
496
|
+
* - Creates a cron entry that starts the daemon at the start time
|
|
497
|
+
* - Creates a cron entry that stops the daemon at the stop time
|
|
498
|
+
*/
|
|
499
|
+
export function installSchedule(projectPath, schedule) {
|
|
500
|
+
const startTime = parseTime(schedule.start);
|
|
501
|
+
const stopTime = parseTime(schedule.stop);
|
|
502
|
+
if (!startTime || !stopTime) {
|
|
503
|
+
return { success: false, error: `Invalid time format. Use HH:MM (24h), got start="${schedule.start}" stop="${schedule.stop}"` };
|
|
504
|
+
}
|
|
505
|
+
const startTag = getScheduleTag(projectPath, "start");
|
|
506
|
+
const stopTag = getScheduleTag(projectPath, "stop");
|
|
507
|
+
const daemonJs = path.join(path.dirname(fileURLToPath(import.meta.url)), "daemon.js").replace(/\\/g, "/");
|
|
508
|
+
const daysCron = daysToCron(schedule.days);
|
|
509
|
+
try {
|
|
510
|
+
if (isWindows) {
|
|
511
|
+
// Windows: schtasks for start
|
|
512
|
+
const startCmd = `powershell -NoProfile -Command "Start-Process node -ArgumentList '${daemonJs}','--verbose' -WindowStyle Hidden -WorkingDirectory '${projectPath.replace(/\\/g, "/")}'"`;
|
|
513
|
+
const stopCmd = `powershell -NoProfile -Command "` +
|
|
514
|
+
`$lock='${path.join(projectPath, ".robot-chat", "daemon.lock").replace(/\\/g, "/")}'; ` +
|
|
515
|
+
`if (Test-Path $lock) { ` +
|
|
516
|
+
`$pid = Get-Content $lock; ` +
|
|
517
|
+
`Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue; ` +
|
|
518
|
+
`Remove-Item $lock -Force -ErrorAction SilentlyContinue ` +
|
|
519
|
+
`}"`;
|
|
520
|
+
// Remove existing tasks
|
|
521
|
+
for (const tag of [startTag, stopTag]) {
|
|
522
|
+
try {
|
|
523
|
+
execSync(`schtasks /Delete /TN "${tag}" /F`, { stdio: "ignore", windowsHide: true });
|
|
524
|
+
}
|
|
525
|
+
catch { /* ok */ }
|
|
526
|
+
}
|
|
527
|
+
// Create start task
|
|
528
|
+
execSync(`schtasks /Create /TN "${startTag}" /SC DAILY /ST ${schedule.start} /TR "${startCmd}" /F`, { stdio: "ignore", windowsHide: true });
|
|
529
|
+
// Create stop task
|
|
530
|
+
execSync(`schtasks /Create /TN "${stopTag}" /SC DAILY /ST ${schedule.stop} /TR "${stopCmd}" /F`, { stdio: "ignore", windowsHide: true });
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
// Unix: crontab entries
|
|
534
|
+
let existingCrontab = "";
|
|
535
|
+
try {
|
|
536
|
+
existingCrontab = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
|
|
537
|
+
}
|
|
538
|
+
catch { /* no crontab */ }
|
|
539
|
+
// Remove existing schedule entries for this project
|
|
540
|
+
const lines = existingCrontab.split("\n").filter(line => !line.includes(startTag) && !line.includes(stopTag));
|
|
541
|
+
// Build TZ prefix if timezone specified
|
|
542
|
+
const tzPrefix = schedule.timezone ? `TZ=${schedule.timezone} ` : "";
|
|
543
|
+
// Add start cron: at start time, launch daemon
|
|
544
|
+
const startCron = `${tzPrefix}${startTime.minute} ${startTime.hour} * * ${daysCron} ROBOT_CHAT_PROJECT_PATH="${projectPath}" node "${daemonJs}" --verbose >> "${path.join(projectPath, ".robot-chat", "daemon-schedule.log").replace(/\\/g, "/")}" 2>&1 & # ${startTag}`;
|
|
545
|
+
// Add stop cron: at stop time, kill daemon
|
|
546
|
+
const lockFile = path.join(projectPath, ".robot-chat", "daemon.lock").replace(/\\/g, "/");
|
|
547
|
+
const stopCron = `${tzPrefix}${stopTime.minute} ${stopTime.hour} * * ${daysCron} if [ -f "${lockFile}" ]; then kill $(cat "${lockFile}") 2>/dev/null; rm -f "${lockFile}"; fi # ${stopTag}`;
|
|
548
|
+
lines.push(startCron);
|
|
549
|
+
lines.push(stopCron);
|
|
550
|
+
const newCrontab = lines.filter(l => l.trim()).join("\n") + "\n";
|
|
551
|
+
execSync(`echo "${newCrontab.replace(/"/g, '\\"')}" | crontab -`, {
|
|
552
|
+
encoding: "utf-8",
|
|
553
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
// Save schedule to config
|
|
557
|
+
saveConfig(projectPath, { schedule });
|
|
558
|
+
return {
|
|
559
|
+
success: true,
|
|
560
|
+
details: `Daemon will start at ${schedule.start} and stop at ${schedule.stop}` +
|
|
561
|
+
(schedule.days ? ` on ${schedule.days.join(", ")}` : " daily") +
|
|
562
|
+
(schedule.timezone ? ` (${schedule.timezone})` : ""),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
return {
|
|
567
|
+
success: false,
|
|
568
|
+
error: `Failed to install schedule: ${err instanceof Error ? err.message : String(err)}`,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Remove scheduled start/stop cron entries for a project.
|
|
574
|
+
*/
|
|
575
|
+
export function removeSchedule(projectPath) {
|
|
576
|
+
const startTag = getScheduleTag(projectPath, "start");
|
|
577
|
+
const stopTag = getScheduleTag(projectPath, "stop");
|
|
578
|
+
try {
|
|
579
|
+
if (isWindows) {
|
|
580
|
+
for (const tag of [startTag, stopTag]) {
|
|
581
|
+
try {
|
|
582
|
+
execSync(`schtasks /Delete /TN "${tag}" /F`, { stdio: "ignore", windowsHide: true });
|
|
583
|
+
}
|
|
584
|
+
catch { /* ok */ }
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
let existingCrontab = "";
|
|
589
|
+
try {
|
|
590
|
+
existingCrontab = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
return { success: true };
|
|
594
|
+
}
|
|
595
|
+
const lines = existingCrontab.split("\n").filter(line => !line.includes(startTag) && !line.includes(stopTag));
|
|
596
|
+
const newCrontab = lines.filter(l => l.trim()).join("\n") + "\n";
|
|
597
|
+
execSync(`echo "${newCrontab.replace(/"/g, '\\"')}" | crontab -`, {
|
|
598
|
+
encoding: "utf-8",
|
|
599
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
// Remove schedule from config
|
|
603
|
+
const config = loadConfig(projectPath);
|
|
604
|
+
if (config.schedule) {
|
|
605
|
+
saveConfig(projectPath, { schedule: undefined });
|
|
606
|
+
}
|
|
607
|
+
return { success: true };
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
return {
|
|
611
|
+
success: false,
|
|
612
|
+
error: `Failed to remove schedule: ${err instanceof Error ? err.message : String(err)}`,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Get the current schedule for a project.
|
|
618
|
+
*/
|
|
619
|
+
export function getSchedule(projectPath) {
|
|
620
|
+
const config = loadConfig(projectPath);
|
|
621
|
+
return config.schedule ?? null;
|
|
622
|
+
}
|
|
481
623
|
//# sourceMappingURL=daemon-manager.js.map
|