opencode-jobs 0.2.0 → 1.1.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 +35 -20
- package/dist/cli.js +1055 -75
- package/dist/index.d.ts +1 -1
- package/dist/index.js +274 -137
- package/dist/install.d.ts +3 -0
- package/dist/management.d.ts +9 -0
- package/dist/migration.d.ts +9 -0
- package/dist/paths.d.ts +1 -1
- package/dist/registry.d.ts +1 -0
- package/dist/systemd.d.ts +1 -1
- package/dist/tools.d.ts +1 -1
- package/package.json +1 -1
- package/skill/opencode-jobs/SKILL.md +4 -4
package/dist/index.js
CHANGED
|
@@ -3,16 +3,8 @@
|
|
|
3
3
|
import {
|
|
4
4
|
tool
|
|
5
5
|
} from "@opencode-ai/plugin";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
existsSync as existsSync4,
|
|
9
|
-
mkdirSync as mkdirSync3,
|
|
10
|
-
openSync,
|
|
11
|
-
readFileSync as readFileSync4,
|
|
12
|
-
rmSync as rmSync2
|
|
13
|
-
} from "fs";
|
|
14
|
-
import { spawn } from "child_process";
|
|
15
|
-
import path6 from "path";
|
|
6
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
|
|
7
|
+
import path7 from "path";
|
|
16
8
|
|
|
17
9
|
// src/cron.ts
|
|
18
10
|
var DOW_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
@@ -149,31 +141,31 @@ function stateRoot() {
|
|
|
149
141
|
return process.env.XDG_STATE_HOME ?? path.join(homedir(), ".local", "state");
|
|
150
142
|
}
|
|
151
143
|
function worktreesDirectory(scopeId) {
|
|
152
|
-
return path.join(stateRoot(), "opencode", "
|
|
144
|
+
return path.join(stateRoot(), "opencode", "jobs", "worktrees", scopeId);
|
|
153
145
|
}
|
|
154
|
-
function
|
|
155
|
-
return path.join(configRoot(), "opencode", "
|
|
146
|
+
function jobsStateDirectory() {
|
|
147
|
+
return path.join(configRoot(), "opencode", "jobs");
|
|
156
148
|
}
|
|
157
149
|
function registryPath() {
|
|
158
|
-
return path.join(
|
|
150
|
+
return path.join(jobsStateDirectory(), "registry.json");
|
|
159
151
|
}
|
|
160
152
|
function scopeDirectory(scopeId) {
|
|
161
|
-
return path.join(
|
|
153
|
+
return path.join(jobsStateDirectory(), "scopes", scopeId);
|
|
162
154
|
}
|
|
163
155
|
function runsDirectory(scopeId) {
|
|
164
|
-
return path.join(
|
|
156
|
+
return path.join(jobsStateDirectory(), "runs", scopeId);
|
|
165
157
|
}
|
|
166
158
|
function runsFile(scopeId, slug) {
|
|
167
159
|
return path.join(runsDirectory(scopeId), `${slug}.jsonl`);
|
|
168
160
|
}
|
|
169
161
|
function sessionStateDirectory(scopeId) {
|
|
170
|
-
return path.join(
|
|
162
|
+
return path.join(jobsStateDirectory(), "sessions", scopeId);
|
|
171
163
|
}
|
|
172
164
|
function sessionStateFile(scopeId, slug) {
|
|
173
165
|
return path.join(sessionStateDirectory(scopeId), `${slug}.txt`);
|
|
174
166
|
}
|
|
175
167
|
function logDirectory(scopeId) {
|
|
176
|
-
return path.join(configRoot(), "opencode", "logs", "
|
|
168
|
+
return path.join(configRoot(), "opencode", "logs", "jobs", scopeId);
|
|
177
169
|
}
|
|
178
170
|
function logFile(scopeId, slug) {
|
|
179
171
|
return path.join(logDirectory(scopeId), `${slug}.log`);
|
|
@@ -182,7 +174,7 @@ function systemdUserDirectory() {
|
|
|
182
174
|
return path.join(configRoot(), "systemd", "user");
|
|
183
175
|
}
|
|
184
176
|
function jobsDirectory(workdir) {
|
|
185
|
-
return path.join(workdir, ".opencode", "
|
|
177
|
+
return path.join(workdir, ".opencode", "jobs");
|
|
186
178
|
}
|
|
187
179
|
function unitBase(scopeId, slug) {
|
|
188
180
|
return `opencode-sched-${scopeId}-${slug}`;
|
|
@@ -447,19 +439,22 @@ var registryFileSchema = z2.object({
|
|
|
447
439
|
version: z2.literal(1),
|
|
448
440
|
projects: z2.record(z2.string(), z2.unknown())
|
|
449
441
|
});
|
|
442
|
+
function readRegistryFile(file) {
|
|
443
|
+
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
444
|
+
const result = registryFileSchema.safeParse(parsed);
|
|
445
|
+
if (!result.success)
|
|
446
|
+
throw new Error(`Invalid job registry: ${file}`);
|
|
447
|
+
const projects = {};
|
|
448
|
+
for (const [key, value] of Object.entries(result.data.projects)) {
|
|
449
|
+
const entry = registryEntrySchema.safeParse(value);
|
|
450
|
+
if (entry.success)
|
|
451
|
+
projects[key] = entry.data;
|
|
452
|
+
}
|
|
453
|
+
return { version: 1, projects };
|
|
454
|
+
}
|
|
450
455
|
function loadRegistry() {
|
|
451
456
|
try {
|
|
452
|
-
|
|
453
|
-
const result = registryFileSchema.safeParse(parsed);
|
|
454
|
-
if (!result.success)
|
|
455
|
-
return { version: 1, projects: {} };
|
|
456
|
-
const projects = {};
|
|
457
|
-
for (const [key, value] of Object.entries(result.data.projects)) {
|
|
458
|
-
const entry = registryEntrySchema.safeParse(value);
|
|
459
|
-
if (entry.success)
|
|
460
|
-
projects[key] = entry.data;
|
|
461
|
-
}
|
|
462
|
-
return { version: 1, projects };
|
|
457
|
+
return readRegistryFile(registryPath());
|
|
463
458
|
} catch {
|
|
464
459
|
return { version: 1, projects: {} };
|
|
465
460
|
}
|
|
@@ -548,7 +543,7 @@ import { spawnSync } from "child_process";
|
|
|
548
543
|
import path4 from "path";
|
|
549
544
|
import { homedir as homedir2 } from "os";
|
|
550
545
|
function findOpencode() {
|
|
551
|
-
const override = process.env.OPENCODE_SCHEDULER_OPENCODE_PATH;
|
|
546
|
+
const override = process.env.OPENCODE_JOBS_OPENCODE_PATH ?? process.env.OPENCODE_SCHEDULER_OPENCODE_PATH;
|
|
552
547
|
if (override !== undefined && override.length > 0)
|
|
553
548
|
return override;
|
|
554
549
|
const which = spawnSync("sh", ["-c", "command -v opencode"], {
|
|
@@ -581,24 +576,24 @@ function guardScriptLines(guard) {
|
|
|
581
576
|
];
|
|
582
577
|
}
|
|
583
578
|
function worktreeDefaultRoot(scopeId) {
|
|
584
|
-
return "${XDG_STATE_HOME:-$HOME/.local/state}/opencode/
|
|
579
|
+
return "${XDG_STATE_HOME:-$HOME/.local/state}/opencode/jobs/worktrees/" + scopeId;
|
|
585
580
|
}
|
|
586
581
|
function worktreePrologueLines(job, scopeId) {
|
|
587
582
|
const base = job.worktree?.base;
|
|
588
583
|
return [
|
|
589
584
|
"wt_enabled=1",
|
|
590
585
|
'orig_pwd="$(pwd)"',
|
|
591
|
-
'lock_dir="$config_root/opencode/
|
|
586
|
+
'lock_dir="$config_root/opencode/jobs/locks/$scope"',
|
|
592
587
|
'mkdir -p "$lock_dir"',
|
|
593
588
|
'exec 9>"$lock_dir/$slug.lock"',
|
|
594
589
|
"if ! flock -n 9; then",
|
|
595
|
-
' echo "
|
|
590
|
+
' echo "opencode-jobs: another run of $slug is already active, skipping"',
|
|
596
591
|
" finish skipped 0",
|
|
597
592
|
" exit 0",
|
|
598
593
|
"fi",
|
|
599
594
|
base === undefined ? `wt_root="${worktreeDefaultRoot(scopeId)}"` : `wt_root=${shQuote(base)}`,
|
|
600
595
|
'if ! mkdir -p "$wt_root"; then',
|
|
601
|
-
' echo "
|
|
596
|
+
' echo "opencode-jobs: cannot create worktree base $wt_root"',
|
|
602
597
|
" finish failed 1",
|
|
603
598
|
" exit 1",
|
|
604
599
|
"fi",
|
|
@@ -613,31 +608,31 @@ function worktreePrologueLines(job, scopeId) {
|
|
|
613
608
|
" wt_stale_saved=1",
|
|
614
609
|
' git -C "$wt_path" add -A >/dev/null 2>&1 || wt_stale_saved=0',
|
|
615
610
|
' if [ "$wt_stale_saved" -eq 1 ] && ! git -C "$wt_path" diff --cached --quiet >/dev/null 2>&1; then',
|
|
616
|
-
' git -C "$wt_path" -c user.name=opencode-jobs -c user.email=
|
|
611
|
+
' git -C "$wt_path" -c user.name=opencode-jobs -c user.email=jobs@opencode.invalid commit --no-gpg-sign -m "opencode-jobs: $slug recovery (stale worktree)" >/dev/null 2>&1 || wt_stale_saved=0',
|
|
617
612
|
" fi",
|
|
618
613
|
' if [ "$wt_stale_saved" -eq 1 ]; then',
|
|
619
614
|
' git worktree remove --force "$wt_path" >/dev/null 2>&1',
|
|
620
615
|
' rm -rf "$wt_path"',
|
|
621
616
|
" else",
|
|
622
|
-
' echo "
|
|
617
|
+
' echo "opencode-jobs: cannot save changes in stale worktree $wt_path; keeping it and aborting this run"',
|
|
623
618
|
' wt_branch=""',
|
|
624
619
|
" finish failed 1",
|
|
625
620
|
" exit 1",
|
|
626
621
|
" fi",
|
|
627
622
|
" else",
|
|
628
|
-
' echo "
|
|
623
|
+
' echo "opencode-jobs: removing unexpected directory at $wt_path"',
|
|
629
624
|
' rm -rf "$wt_path"',
|
|
630
625
|
" fi",
|
|
631
626
|
" git worktree prune >/dev/null 2>&1",
|
|
632
627
|
"fi",
|
|
633
628
|
'if ! git worktree add -b "$wt_branch" "$wt_path" "$wt_base_ref"; then',
|
|
634
|
-
' echo "
|
|
629
|
+
' echo "opencode-jobs: failed to create worktree $wt_path (worktree jobs require a git repository)"',
|
|
635
630
|
' wt_branch=""',
|
|
636
631
|
" finish failed 1",
|
|
637
632
|
" exit 1",
|
|
638
633
|
"fi",
|
|
639
634
|
'if [ -n "$wt_sub" ] && [ ! -d "$wt_path/$wt_sub" ]; then',
|
|
640
|
-
' echo "
|
|
635
|
+
' echo "opencode-jobs: project subdirectory $wt_sub is missing from the worktree at $wt_base_ref"',
|
|
641
636
|
' git -C "$orig_pwd" worktree remove --force "$wt_path" >/dev/null 2>&1',
|
|
642
637
|
' rm -rf "$wt_path"',
|
|
643
638
|
' wt_branch=""',
|
|
@@ -659,10 +654,10 @@ function worktreeEpilogueLines(options) {
|
|
|
659
654
|
' git -C "$wt_path" add -A >/dev/null 2>&1',
|
|
660
655
|
" wt_keep=0",
|
|
661
656
|
' if ! git -C "$wt_path" diff --cached --quiet >/dev/null 2>&1; then',
|
|
662
|
-
' if git -C "$wt_path" -c user.name=opencode-jobs -c user.email=
|
|
663
|
-
' echo "
|
|
657
|
+
' if git -C "$wt_path" -c user.name=opencode-jobs -c user.email=jobs@opencode.invalid commit --no-gpg-sign -m "$wt_msg" >/dev/null 2>&1; then',
|
|
658
|
+
' echo "opencode-jobs: committed worktree changes to branch $wt_branch"',
|
|
664
659
|
" else",
|
|
665
|
-
' echo "
|
|
660
|
+
' echo "opencode-jobs: worktree commit failed, keeping worktree at $wt_path"',
|
|
666
661
|
" wt_keep=1",
|
|
667
662
|
" fi",
|
|
668
663
|
" fi",
|
|
@@ -688,7 +683,7 @@ function compactSessionLines() {
|
|
|
688
683
|
"compact_session() {",
|
|
689
684
|
' csid="$1"',
|
|
690
685
|
" if ! command -v curl >/dev/null 2>&1; then",
|
|
691
|
-
' echo "
|
|
686
|
+
' echo "opencode-jobs: curl not available, skipping compaction"',
|
|
692
687
|
" return 0",
|
|
693
688
|
" fi",
|
|
694
689
|
' serve_out="$(mktemp)"',
|
|
@@ -705,7 +700,7 @@ function compactSessionLines() {
|
|
|
705
700
|
" tries=$((tries + 1))",
|
|
706
701
|
" done",
|
|
707
702
|
' if [ -z "$serve_port" ]; then',
|
|
708
|
-
' echo "
|
|
703
|
+
' echo "opencode-jobs: compaction server failed to start"',
|
|
709
704
|
' sed -n "1,10p" "$serve_err" >&2',
|
|
710
705
|
' kill "$serve_pid" 2>/dev/null',
|
|
711
706
|
' wait "$serve_pid" 2>/dev/null',
|
|
@@ -724,7 +719,7 @@ function compactSessionLines() {
|
|
|
724
719
|
" tries=$((tries + 1))",
|
|
725
720
|
" done",
|
|
726
721
|
' if [ "$healthy" -ne 1 ]; then',
|
|
727
|
-
' echo "
|
|
722
|
+
' echo "opencode-jobs: compaction server never became healthy"',
|
|
728
723
|
' kill "$serve_pid" 2>/dev/null',
|
|
729
724
|
' wait "$serve_pid" 2>/dev/null',
|
|
730
725
|
' rm -f "$serve_out" "$serve_err"',
|
|
@@ -744,22 +739,22 @@ function compactSessionLines() {
|
|
|
744
739
|
String.raw` cs_model="$(printf '%s\n' "$defaults" | sed -n '${DEFAULT_MODEL_SED}' | head -n 1)"`,
|
|
745
740
|
" fi",
|
|
746
741
|
' if [ -z "$cs_provider" ] || [ -z "$cs_model" ]; then',
|
|
747
|
-
' echo "
|
|
742
|
+
' echo "opencode-jobs: could not resolve a model for compaction, skipping"',
|
|
748
743
|
' kill "$serve_pid" 2>/dev/null',
|
|
749
744
|
' wait "$serve_pid" 2>/dev/null',
|
|
750
745
|
' rm -f "$serve_out" "$serve_err"',
|
|
751
746
|
" return 0",
|
|
752
747
|
" fi",
|
|
753
|
-
' echo "
|
|
748
|
+
' echo "opencode-jobs: compacting session $csid (mode: $session_mode)"',
|
|
754
749
|
String.raw` result="$(curl -s --max-time 900 -X POST -H 'content-type: application/json' -d "{\"providerID\":\"$cs_provider\",\"modelID\":\"$cs_model\"}" "http://127.0.0.1:$serve_port/session/$csid/summarize")"`,
|
|
755
750
|
' if [ "$result" != "true" ]; then',
|
|
756
|
-
' echo "
|
|
751
|
+
' echo "opencode-jobs: compaction failed: $result"',
|
|
757
752
|
" fi",
|
|
758
753
|
' if [ "$result" = "true" ] && [ "$oc_keep_last" -eq 1 ] && [ -n "$cs_text" ]; then',
|
|
759
754
|
String.raw` inject_body="{\"noReply\":true,\"parts\":[{\"type\":\"text\",\"text\":\"$cs_text\"}]}"`,
|
|
760
755
|
` http="$(curl -s -o /dev/null -w '%{http_code}' --max-time 120 -X POST -H 'content-type: application/json' -d "$inject_body" "http://127.0.0.1:$serve_port/session/$csid/message")"`,
|
|
761
756
|
' if [ "$http" != "200" ]; then',
|
|
762
|
-
' echo "
|
|
757
|
+
' echo "opencode-jobs: keeping last result failed (HTTP $http)"',
|
|
763
758
|
" fi",
|
|
764
759
|
" fi",
|
|
765
760
|
' kill "$serve_pid" 2>/dev/null',
|
|
@@ -780,25 +775,25 @@ function runScriptContent(job, scopeId, opencodeBin) {
|
|
|
780
775
|
`scope=${shQuote(scopeId)}`,
|
|
781
776
|
`oc_bin=${shQuote(opencodeBin)}`,
|
|
782
777
|
'config_root="${XDG_CONFIG_HOME:-$HOME/.config}"',
|
|
783
|
-
'runs="$config_root/opencode/
|
|
778
|
+
'runs="$config_root/opencode/jobs/runs/$scope"',
|
|
784
779
|
'mkdir -p "$runs"',
|
|
785
780
|
'record_file="$runs/$slug.jsonl"',
|
|
786
781
|
...isTracked ? [
|
|
787
|
-
'sessions="$config_root/opencode/
|
|
782
|
+
'sessions="$config_root/opencode/jobs/sessions/$scope"',
|
|
788
783
|
'mkdir -p "$sessions"',
|
|
789
784
|
'state_file="$sessions/$slug.txt"',
|
|
790
785
|
`session_mode=${shQuote(mode)}`,
|
|
791
786
|
'prev_session=""',
|
|
792
787
|
'if [ -f "$state_file" ]; then prev_session=$(cat "$state_file"); fi'
|
|
793
788
|
] : [],
|
|
794
|
-
'started_by="${
|
|
789
|
+
'started_by="${OPENCODE_JOBS_STARTED_BY:-scheduled}"',
|
|
795
790
|
'run_id="$(date +%s%N)-$$"',
|
|
796
791
|
"started=$(date +%s)",
|
|
797
792
|
'new_session=""',
|
|
798
793
|
'wt_branch=""',
|
|
799
794
|
'wt_commit=""',
|
|
800
795
|
`export OPENCODE_PERMISSION='{"question":"deny"}'`,
|
|
801
|
-
'export
|
|
796
|
+
'export OPENCODE_JOBS_RUN_ID="$run_id"',
|
|
802
797
|
"finish() {",
|
|
803
798
|
' status="$1"',
|
|
804
799
|
' code="$2"',
|
|
@@ -839,7 +834,7 @@ function runScriptContent(job, scopeId, opencodeBin) {
|
|
|
839
834
|
'cat "$json_out"',
|
|
840
835
|
...extractSessionIdLines("new_session"),
|
|
841
836
|
'if [ "$code" -ne 0 ] && [ -n "$prev_session" ] && [ -z "$new_session" ] && grep -qi "session not found" "$json_out"; then',
|
|
842
|
-
' echo "
|
|
837
|
+
' echo "opencode-jobs: session $prev_session not found, retrying with a fresh session"',
|
|
843
838
|
' rm -f "$json_out"',
|
|
844
839
|
' json_out="$(mktemp)"',
|
|
845
840
|
' run_opencode "" 1 >"$json_out" 2>&1',
|
|
@@ -948,6 +943,16 @@ Hint: no systemd user session is reachable. Over SSH try enabling lingering: log
|
|
|
948
943
|
}
|
|
949
944
|
return "";
|
|
950
945
|
}
|
|
946
|
+
function isTimerLoaded(base) {
|
|
947
|
+
const result = systemctl([
|
|
948
|
+
"show",
|
|
949
|
+
timerUnit(base),
|
|
950
|
+
"-p",
|
|
951
|
+
"LoadState",
|
|
952
|
+
"--value"
|
|
953
|
+
]);
|
|
954
|
+
return result.ok && result.stdout !== "not-found";
|
|
955
|
+
}
|
|
951
956
|
function timerStatus(base) {
|
|
952
957
|
const result = systemctl([
|
|
953
958
|
"show",
|
|
@@ -985,17 +990,25 @@ function writeJobUnits(job, workdir, scopeId, opencodeBin, pathEnvironment) {
|
|
|
985
990
|
}
|
|
986
991
|
function removeJobUnits(scopeId, slug) {
|
|
987
992
|
const base = unitBase(scopeId, slug);
|
|
988
|
-
|
|
989
|
-
for (const file of [
|
|
993
|
+
const files = [
|
|
990
994
|
path4.join(systemdUserDirectory(), timerUnit(base)),
|
|
991
995
|
path4.join(systemdUserDirectory(), serviceUnit(base))
|
|
992
|
-
]
|
|
996
|
+
];
|
|
997
|
+
const script = runScriptPath(scopeId, slug);
|
|
998
|
+
if ([...files, script].every((file) => !existsSync3(file)) && !isTimerLoaded(base)) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const disable = systemctl(["disable", "--now", timerUnit(base)]);
|
|
1002
|
+
if (!disable.ok) {
|
|
1003
|
+
return `${timerUnit(base)}: ${disable.stderr}${systemdHint(disable.stderr)}`;
|
|
1004
|
+
}
|
|
1005
|
+
for (const file of files) {
|
|
993
1006
|
if (existsSync3(file))
|
|
994
1007
|
rmSync(file);
|
|
995
1008
|
}
|
|
996
|
-
const script = runScriptPath(scopeId, slug);
|
|
997
1009
|
if (existsSync3(script))
|
|
998
1010
|
rmSync(script);
|
|
1011
|
+
return;
|
|
999
1012
|
}
|
|
1000
1013
|
function removeStaleUnits(scopeId, expectedSlugs) {
|
|
1001
1014
|
const prefix = `opencode-sched-${scopeId}-`;
|
|
@@ -1072,7 +1085,7 @@ ${errors.join(`
|
|
|
1072
1085
|
lines.push(`Removed stale units for deleted jobs: ${removed.join(", ")}`);
|
|
1073
1086
|
lines.push(...describeJobSchedules(jobs, scopeId));
|
|
1074
1087
|
if (failures.length > 0)
|
|
1075
|
-
|
|
1088
|
+
throw new Error(`Timer activation failures:
|
|
1076
1089
|
${failures.join(`
|
|
1077
1090
|
`)}`);
|
|
1078
1091
|
return lines.join(`
|
|
@@ -1093,9 +1106,19 @@ function disableProject(workdir) {
|
|
|
1093
1106
|
const entry = registryEntry(abs);
|
|
1094
1107
|
if (entry === undefined)
|
|
1095
1108
|
return `Project is not enabled: ${abs}`;
|
|
1096
|
-
|
|
1097
|
-
removeJobUnits(entry.scopeId, slug);
|
|
1098
|
-
|
|
1109
|
+
const failures = entry.jobs.flatMap((slug) => {
|
|
1110
|
+
const failure = removeJobUnits(entry.scopeId, slug);
|
|
1111
|
+
return failure === undefined ? [] : [failure];
|
|
1112
|
+
});
|
|
1113
|
+
if (failures.length > 0) {
|
|
1114
|
+
throw new Error(`Timer removal failures:
|
|
1115
|
+
${failures.join(`
|
|
1116
|
+
`)}`);
|
|
1117
|
+
}
|
|
1118
|
+
const reload = systemctl(["daemon-reload"]);
|
|
1119
|
+
if (!reload.ok) {
|
|
1120
|
+
throw new Error(`systemctl --user daemon-reload failed: ${reload.stderr}${systemdHint(reload.stderr)}`);
|
|
1121
|
+
}
|
|
1099
1122
|
const registry = loadRegistry();
|
|
1100
1123
|
const { [abs]: _omitted, ...remainingProjects } = registry.projects;
|
|
1101
1124
|
registry.projects = remainingProjects;
|
|
@@ -1108,13 +1131,10 @@ function disableProject(workdir) {
|
|
|
1108
1131
|
`);
|
|
1109
1132
|
}
|
|
1110
1133
|
|
|
1111
|
-
// src/
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
function fail(message) {
|
|
1116
|
-
return { output: `Error: ${message}`, metadata: { error: true } };
|
|
1117
|
-
}
|
|
1134
|
+
// src/management.ts
|
|
1135
|
+
import { spawn } from "child_process";
|
|
1136
|
+
import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync } from "fs";
|
|
1137
|
+
import path6 from "path";
|
|
1118
1138
|
function tryParseCron(schedule) {
|
|
1119
1139
|
try {
|
|
1120
1140
|
return parseCron(schedule);
|
|
@@ -1122,7 +1142,7 @@ function tryParseCron(schedule) {
|
|
|
1122
1142
|
return;
|
|
1123
1143
|
}
|
|
1124
1144
|
}
|
|
1125
|
-
function
|
|
1145
|
+
function listJobs(directory) {
|
|
1126
1146
|
const { jobs, errors } = loadJobs(directory);
|
|
1127
1147
|
const entry = registryEntry(directory);
|
|
1128
1148
|
const header = entry ? `Project enabled (scope ${entry.scopeId}). Job definitions: ${jobsDirectory(directory)}` : `Project not enabled. Job definitions: ${jobsDirectory(directory)}`;
|
|
@@ -1144,8 +1164,63 @@ function listJobsOutput(directory) {
|
|
|
1144
1164
|
lines.push(`- ${job.slug}: ${job.schedule} (${describeCron(sets)})${nextDesc}${lastDesc}`);
|
|
1145
1165
|
}
|
|
1146
1166
|
lines.push(...errors.map((error) => `! ${error}`));
|
|
1147
|
-
return ok
|
|
1148
|
-
`)
|
|
1167
|
+
return { ok: true, output: lines.join(`
|
|
1168
|
+
`) };
|
|
1169
|
+
}
|
|
1170
|
+
function runJobNow(slugInput, directory) {
|
|
1171
|
+
const slug = slugify(slugInput);
|
|
1172
|
+
const file = path6.join(jobsDirectory(directory), `${slug}.json`);
|
|
1173
|
+
if (!existsSync4(file)) {
|
|
1174
|
+
return {
|
|
1175
|
+
ok: false,
|
|
1176
|
+
output: `No job "${slug}" in ${jobsDirectory(directory)}`
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
const entry = registryEntry(directory);
|
|
1180
|
+
if (entry === undefined) {
|
|
1181
|
+
return {
|
|
1182
|
+
ok: false,
|
|
1183
|
+
output: `Project is not enabled, so no run script exists for "${slug}". Run enable_project first.`
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
const script = runScriptPath(entry.scopeId, slug);
|
|
1187
|
+
if (!existsSync4(script)) {
|
|
1188
|
+
return {
|
|
1189
|
+
ok: false,
|
|
1190
|
+
output: `Run script missing for "${slug}". Run enable_project to (re)install units.`
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
const log = logFile(entry.scopeId, slug);
|
|
1194
|
+
mkdirSync3(logDirectory(entry.scopeId), { recursive: true });
|
|
1195
|
+
const fd = openSync(log, "a");
|
|
1196
|
+
const child = spawn("/bin/sh", [script], {
|
|
1197
|
+
cwd: path6.resolve(directory),
|
|
1198
|
+
env: { ...process.env, OPENCODE_JOBS_STARTED_BY: "manual" },
|
|
1199
|
+
stdio: ["ignore", fd, fd]
|
|
1200
|
+
});
|
|
1201
|
+
child.unref();
|
|
1202
|
+
closeSync(fd);
|
|
1203
|
+
const tail = tailFile(log, 5, 2000);
|
|
1204
|
+
const parts = [
|
|
1205
|
+
`Started "${slug}" manually (pid ${String(child.pid)})`,
|
|
1206
|
+
`Log: ${log}`
|
|
1207
|
+
];
|
|
1208
|
+
if (tail?.length)
|
|
1209
|
+
parts.push(`Log tail:
|
|
1210
|
+
${tail}`);
|
|
1211
|
+
return { ok: true, output: parts.join(`
|
|
1212
|
+
`) };
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/tools.ts
|
|
1216
|
+
function ok(output) {
|
|
1217
|
+
return { output };
|
|
1218
|
+
}
|
|
1219
|
+
function fail(message) {
|
|
1220
|
+
return { output: `Error: ${message}`, metadata: { error: true } };
|
|
1221
|
+
}
|
|
1222
|
+
function managementToolResult(result) {
|
|
1223
|
+
return result.ok ? ok(result.output) : fail(result.output);
|
|
1149
1224
|
}
|
|
1150
1225
|
function scheduleJobOutput(input, directory) {
|
|
1151
1226
|
const slug = slugify(input.slug ?? input.name);
|
|
@@ -1189,7 +1264,7 @@ function scheduleJobOutput(input, directory) {
|
|
|
1189
1264
|
} catch (error) {
|
|
1190
1265
|
return fail(errorMessage(error));
|
|
1191
1266
|
}
|
|
1192
|
-
const existing = loadJobFile(
|
|
1267
|
+
const existing = loadJobFile(path7.join(jobsDirectory(directory), `${slug}.json`), slug);
|
|
1193
1268
|
const job = {
|
|
1194
1269
|
slug,
|
|
1195
1270
|
name: input.name,
|
|
@@ -1203,7 +1278,7 @@ function scheduleJobOutput(input, directory) {
|
|
|
1203
1278
|
updatedAt: nowIso()
|
|
1204
1279
|
};
|
|
1205
1280
|
saveJob(directory, job);
|
|
1206
|
-
const relativePath = `.opencode/
|
|
1281
|
+
const relativePath = `.opencode/jobs/${slug}.json`;
|
|
1207
1282
|
const lines = [
|
|
1208
1283
|
`${existing.ok ? "Updated" : "Created"} job "${job.name}" (${slug})`,
|
|
1209
1284
|
`Definition: ${relativePath} (${job.schedule} \u2014 ${describeCron(sets)})`
|
|
@@ -1218,7 +1293,7 @@ function scheduleJobOutput(input, directory) {
|
|
|
1218
1293
|
return ok(lines.join(`
|
|
1219
1294
|
`));
|
|
1220
1295
|
}
|
|
1221
|
-
const abs =
|
|
1296
|
+
const abs = path7.resolve(directory);
|
|
1222
1297
|
const opencodeBin = findOpencode();
|
|
1223
1298
|
const pathEnvironment = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
|
1224
1299
|
const base = writeJobUnits(job, abs, entry.scopeId, opencodeBin, pathEnvironment);
|
|
@@ -1244,8 +1319,8 @@ function scheduleJobOutput(input, directory) {
|
|
|
1244
1319
|
`));
|
|
1245
1320
|
}
|
|
1246
1321
|
function showJobOutput(slugInput, directory) {
|
|
1247
|
-
const file =
|
|
1248
|
-
if (!
|
|
1322
|
+
const file = path7.join(jobsDirectory(directory), `${slugify(slugInput)}.json`);
|
|
1323
|
+
if (!existsSync5(file)) {
|
|
1249
1324
|
return fail(`No job "${slugInput}" in ${jobsDirectory(directory)}. Use list_jobs to see definitions.`);
|
|
1250
1325
|
}
|
|
1251
1326
|
const result = loadJobFile(file);
|
|
@@ -1259,7 +1334,7 @@ function showJobOutput(slugInput, directory) {
|
|
|
1259
1334
|
const lines = [
|
|
1260
1335
|
`${job.name} (${job.slug})`,
|
|
1261
1336
|
`Schedule: ${job.schedule} \u2014 ${describeCron(sets)}`,
|
|
1262
|
-
`Definition: .opencode/
|
|
1337
|
+
`Definition: .opencode/jobs/${job.slug}.json (updated ${job.updatedAt})`,
|
|
1263
1338
|
`Run: ${runDesc}`
|
|
1264
1339
|
];
|
|
1265
1340
|
if (job.guard !== undefined)
|
|
@@ -1270,7 +1345,7 @@ function showJobOutput(slugInput, directory) {
|
|
|
1270
1345
|
}
|
|
1271
1346
|
if (job.session !== undefined) {
|
|
1272
1347
|
const state = sessionStateFile(scopeId, job.slug);
|
|
1273
|
-
const sessionId =
|
|
1348
|
+
const sessionId = existsSync5(state) ? readFileSync4(state, "utf8").trim() : "";
|
|
1274
1349
|
lines.push(`Session: ${job.session}${sessionId.length > 0 ? ` \u2014 current ${sessionId}` : " \u2014 no session yet"}`);
|
|
1275
1350
|
}
|
|
1276
1351
|
if (job.run.agent !== undefined)
|
|
@@ -1305,24 +1380,30 @@ function showJobOutput(slugInput, directory) {
|
|
|
1305
1380
|
}
|
|
1306
1381
|
function removeJobDefinitionOutput(slugInput, directory) {
|
|
1307
1382
|
const slug = slugify(slugInput);
|
|
1308
|
-
const file =
|
|
1309
|
-
if (!
|
|
1383
|
+
const file = path7.join(jobsDirectory(directory), `${slug}.json`);
|
|
1384
|
+
if (!existsSync5(file))
|
|
1310
1385
|
return fail(`No job "${slug}" in ${jobsDirectory(directory)}`);
|
|
1386
|
+
const abs = path7.resolve(directory);
|
|
1387
|
+
const entry = registryEntry(directory);
|
|
1388
|
+
if (entry?.jobs.includes(slug)) {
|
|
1389
|
+
const removalFailure = removeJobUnits(entry.scopeId, slug);
|
|
1390
|
+
if (removalFailure !== undefined) {
|
|
1391
|
+
return fail(`Failed to remove systemd units: ${removalFailure}`);
|
|
1392
|
+
}
|
|
1393
|
+
const reload = systemctl(["daemon-reload"]);
|
|
1394
|
+
if (!reload.ok) {
|
|
1395
|
+
return fail(`Removed the units but systemd reload failed: ${reload.stderr}${systemdHint(reload.stderr)}`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1311
1398
|
rmSync2(file);
|
|
1312
|
-
const lines = [
|
|
1313
|
-
|
|
1314
|
-
];
|
|
1315
|
-
const scopeId = registryEntry(directory)?.scopeId ?? deriveScopeId(directory);
|
|
1399
|
+
const lines = [`Deleted job definition .opencode/jobs/${slug}.json`];
|
|
1400
|
+
const scopeId = entry?.scopeId ?? deriveScopeId(directory);
|
|
1316
1401
|
const state = sessionStateFile(scopeId, slug);
|
|
1317
|
-
if (
|
|
1402
|
+
if (existsSync5(state)) {
|
|
1318
1403
|
rmSync2(state);
|
|
1319
1404
|
lines.push(`Removed session state ${state}`);
|
|
1320
1405
|
}
|
|
1321
|
-
const abs = path6.resolve(directory);
|
|
1322
|
-
const entry = registryEntry(directory);
|
|
1323
1406
|
if (entry?.jobs.includes(slug)) {
|
|
1324
|
-
removeJobUnits(entry.scopeId, slug);
|
|
1325
|
-
systemctl(["daemon-reload"]);
|
|
1326
1407
|
const registry = loadRegistry();
|
|
1327
1408
|
const current = registry.projects[abs];
|
|
1328
1409
|
if (current !== undefined) {
|
|
@@ -1341,40 +1422,6 @@ function omitProject(registry, workdir) {
|
|
|
1341
1422
|
const { [workdir]: _omitted, ...remaining } = registry.projects;
|
|
1342
1423
|
registry.projects = remaining;
|
|
1343
1424
|
}
|
|
1344
|
-
function runJobNowOutput(slugInput, directory) {
|
|
1345
|
-
const slug = slugify(slugInput);
|
|
1346
|
-
const file = path6.join(jobsDirectory(directory), `${slug}.json`);
|
|
1347
|
-
if (!existsSync4(file))
|
|
1348
|
-
return fail(`No job "${slug}" in ${jobsDirectory(directory)}`);
|
|
1349
|
-
const entry = registryEntry(directory);
|
|
1350
|
-
if (entry === undefined) {
|
|
1351
|
-
return fail(`Project is not enabled, so no run script exists for "${slug}". Run enable_project first.`);
|
|
1352
|
-
}
|
|
1353
|
-
const script = runScriptPath(entry.scopeId, slug);
|
|
1354
|
-
if (!existsSync4(script)) {
|
|
1355
|
-
return fail(`Run script missing for "${slug}". Run enable_project to (re)install units.`);
|
|
1356
|
-
}
|
|
1357
|
-
const log = logFile(entry.scopeId, slug);
|
|
1358
|
-
mkdirSync3(logDirectory(entry.scopeId), { recursive: true });
|
|
1359
|
-
const fd = openSync(log, "a");
|
|
1360
|
-
const child = spawn("/bin/sh", [script], {
|
|
1361
|
-
cwd: path6.resolve(directory),
|
|
1362
|
-
env: { ...process.env, OPENCODE_SCHEDULER_STARTED_BY: "manual" },
|
|
1363
|
-
stdio: ["ignore", fd, fd]
|
|
1364
|
-
});
|
|
1365
|
-
child.unref();
|
|
1366
|
-
closeSync(fd);
|
|
1367
|
-
const tail = tailFile(log, 5, 2000);
|
|
1368
|
-
const parts = [
|
|
1369
|
-
`Started "${slug}" manually (pid ${String(child.pid)})`,
|
|
1370
|
-
`Log: ${log}`
|
|
1371
|
-
];
|
|
1372
|
-
if (tail?.length)
|
|
1373
|
-
parts.push(`Log tail:
|
|
1374
|
-
${tail}`);
|
|
1375
|
-
return ok(parts.join(`
|
|
1376
|
-
`));
|
|
1377
|
-
}
|
|
1378
1425
|
function jobLogsOutput(slugInput, lineCountInput, directory) {
|
|
1379
1426
|
const entry = registryEntry(directory);
|
|
1380
1427
|
const scopeId = entry?.scopeId ?? deriveScopeId(directory);
|
|
@@ -1393,21 +1440,21 @@ function listProjectsOutput() {
|
|
|
1393
1440
|
const entries = Object.values(registry.projects).toSorted((a, b) => a.workdir.localeCompare(b.workdir));
|
|
1394
1441
|
if (entries.length === 0)
|
|
1395
1442
|
return ok("No projects with scheduled jobs are registered.");
|
|
1396
|
-
const lines = ["Registry: ~/.config/opencode/
|
|
1443
|
+
const lines = ["Registry: ~/.config/opencode/jobs/registry.json"];
|
|
1397
1444
|
for (const entry of entries) {
|
|
1398
|
-
const missing =
|
|
1445
|
+
const missing = existsSync5(entry.workdir) ? "" : " [WORKDIR MISSING]";
|
|
1399
1446
|
lines.push(`- ${entry.workdir}${missing}`, ` scope ${entry.scopeId}, ${String(entry.jobs.length)} job(s): ${entry.jobs.join(", ")}`);
|
|
1400
1447
|
}
|
|
1401
1448
|
return ok(lines.join(`
|
|
1402
1449
|
`));
|
|
1403
1450
|
}
|
|
1404
1451
|
var listJobsTool = tool({
|
|
1405
|
-
description: "List scheduled job definitions for the current project (from .opencode/
|
|
1452
|
+
description: "List scheduled job definitions for the current project (from .opencode/jobs/), including enabled state, next run, and last run status.",
|
|
1406
1453
|
args: {},
|
|
1407
|
-
execute: (_input, context) => Promise.resolve(
|
|
1454
|
+
execute: (_input, context) => Promise.resolve(managementToolResult(listJobs(context.directory)))
|
|
1408
1455
|
});
|
|
1409
1456
|
var scheduleJobTool = tool({
|
|
1410
|
-
description: "Create or update a scheduled job definition in the current project (.opencode/
|
|
1457
|
+
description: "Create or update a scheduled job definition in the current project (.opencode/jobs/<slug>.json, git-committable). Schedule is a 5-field cron expression. Set either prompt (natural language) or command (custom command name). If the project is enabled, systemd units are re-synced automatically.",
|
|
1411
1458
|
args: {
|
|
1412
1459
|
name: tool.schema.string().describe("Human-readable job name"),
|
|
1413
1460
|
schedule: tool.schema.string().describe('5-field cron expression, e.g. "0 9 * * *" (daily 9am), "0 */6 * * *" (every 6h), "30 8 * * 1" (Mon 8:30)'),
|
|
@@ -1417,7 +1464,7 @@ var scheduleJobTool = tool({
|
|
|
1417
1464
|
session: tool.schema.string().optional().describe(`Session continuity between runs: "new" (default, fresh session each run), "persist" (continue the same session), "compact" (continue the same session; after each run the history is compacted into a summary the next run starts from), "compact+last" (like compact, but the run's final result message is re-injected after the summary so the next run starts from summary plus last result)`),
|
|
1418
1465
|
guard: tool.schema.string().optional().describe('Shell command run before the job; the run only starts if it exits 0, otherwise it is recorded as skipped (applies to run_job too). E.g. "! git diff --quiet" to run only when the repo has changes'),
|
|
1419
1466
|
worktree: tool.schema.boolean().optional().describe("Run the job in a fresh git worktree instead of the project checkout: the worktree is created from worktreeRef (default HEAD), the job runs inside it, all changes are committed to a per-run branch opencode-jobs/<slug>/\u2026, and the worktree is removed afterwards (kept if the safety commit fails). Requires the project to be a git repository"),
|
|
1420
|
-
worktreeBase: tool.schema.string().optional().describe("Parent directory for the worktree (default: ~/.local/state/opencode/
|
|
1467
|
+
worktreeBase: tool.schema.string().optional().describe("Parent directory for the worktree (default: ~/.local/state/opencode/jobs/worktrees/<scopeId>/<slug>). Relative paths resolve against the project directory; it should be dedicated to job worktrees"),
|
|
1421
1468
|
worktreeRef: tool.schema.string().optional().describe('Git ref the worktree branch starts from (default: "HEAD")'),
|
|
1422
1469
|
worktreeCommitMessage: tool.schema.string().optional().describe('Commit message used when saving worktree changes (default: "opencode-jobs: <slug> run <runId>")'),
|
|
1423
1470
|
agent: tool.schema.string().optional().describe("Agent to use for the run"),
|
|
@@ -1446,7 +1493,7 @@ var runJobTool = tool({
|
|
|
1446
1493
|
args: {
|
|
1447
1494
|
slug: tool.schema.string().describe("Job slug to run now")
|
|
1448
1495
|
},
|
|
1449
|
-
execute: (input, context) => Promise.resolve(
|
|
1496
|
+
execute: (input, context) => Promise.resolve(managementToolResult(runJobNow(input.slug, context.directory)))
|
|
1450
1497
|
});
|
|
1451
1498
|
var jobLogsTool = tool({
|
|
1452
1499
|
description: "Show the tail of a scheduled job's log file (scheduled and manual runs both append to it).",
|
|
@@ -1457,7 +1504,7 @@ var jobLogsTool = tool({
|
|
|
1457
1504
|
execute: (input, context) => Promise.resolve(jobLogsOutput(input.slug, input.lines, context.directory))
|
|
1458
1505
|
});
|
|
1459
1506
|
var enableProjectTool = tool({
|
|
1460
|
-
description: "Enable scheduled jobs for the current project: installs a systemd user service+timer per job definition in .opencode/
|
|
1507
|
+
description: "Enable scheduled jobs for the current project: installs a systemd user service+timer per job definition in .opencode/jobs/, registers the project in the global registry (~/.config/opencode/jobs/registry.json), and removes stale units for deleted jobs. Idempotent, so it also re-syncs after job definitions change. Linux only.",
|
|
1461
1508
|
args: {},
|
|
1462
1509
|
execute: (_input, context) => Promise.resolve(enableProjectOutput(context.directory))
|
|
1463
1510
|
});
|
|
@@ -1481,11 +1528,11 @@ function disableProjectOutput(directory) {
|
|
|
1481
1528
|
}
|
|
1482
1529
|
}
|
|
1483
1530
|
var listProjectsTool = tool({
|
|
1484
|
-
description: "List all projects with enabled scheduled jobs from the global registry (~/.config/opencode/
|
|
1531
|
+
description: "List all projects with enabled scheduled jobs from the global registry (~/.config/opencode/jobs/registry.json).",
|
|
1485
1532
|
args: {},
|
|
1486
1533
|
execute: () => Promise.resolve(listProjectsOutput())
|
|
1487
1534
|
});
|
|
1488
|
-
var
|
|
1535
|
+
var jobsTools = {
|
|
1489
1536
|
schedule_job: scheduleJobTool,
|
|
1490
1537
|
list_jobs: listJobsTool,
|
|
1491
1538
|
get_job: showJobTool,
|
|
@@ -1497,12 +1544,102 @@ var schedulerTools = {
|
|
|
1497
1544
|
list_projects: listProjectsTool
|
|
1498
1545
|
};
|
|
1499
1546
|
|
|
1547
|
+
// src/migration.ts
|
|
1548
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, renameSync as renameSync2, rmdirSync } from "fs";
|
|
1549
|
+
import path8 from "path";
|
|
1550
|
+
function legacyJobsStateDirectory() {
|
|
1551
|
+
return path8.join(configRoot(), "opencode", "scheduler");
|
|
1552
|
+
}
|
|
1553
|
+
function legacyRegistryPath() {
|
|
1554
|
+
return path8.join(legacyJobsStateDirectory(), "registry.json");
|
|
1555
|
+
}
|
|
1556
|
+
function legacyDefinitionsDirectory(workdir) {
|
|
1557
|
+
return path8.join(workdir, ".opencode", "scheduler", "jobs");
|
|
1558
|
+
}
|
|
1559
|
+
function migrationMoves(projects) {
|
|
1560
|
+
return [
|
|
1561
|
+
{
|
|
1562
|
+
from: legacyJobsStateDirectory(),
|
|
1563
|
+
to: jobsStateDirectory()
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
from: path8.join(configRoot(), "opencode", "logs", "scheduler"),
|
|
1567
|
+
to: path8.join(configRoot(), "opencode", "logs", "jobs")
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
from: path8.join(stateRoot(), "opencode", "scheduler", "worktrees"),
|
|
1571
|
+
to: path8.join(stateRoot(), "opencode", "jobs", "worktrees")
|
|
1572
|
+
},
|
|
1573
|
+
...[...projects].map((workdir) => ({
|
|
1574
|
+
from: legacyDefinitionsDirectory(workdir),
|
|
1575
|
+
to: jobsDirectory(workdir)
|
|
1576
|
+
}))
|
|
1577
|
+
];
|
|
1578
|
+
}
|
|
1579
|
+
function removeLegacyProjectDirectory(workdir) {
|
|
1580
|
+
try {
|
|
1581
|
+
rmdirSync(path8.join(workdir, ".opencode", "scheduler"));
|
|
1582
|
+
} catch {}
|
|
1583
|
+
}
|
|
1584
|
+
function migrateStorage(projectDirectory, shouldResync) {
|
|
1585
|
+
const project = path8.resolve(projectDirectory);
|
|
1586
|
+
const legacyRegistry = legacyRegistryPath();
|
|
1587
|
+
const canonicalRegistry = path8.join(jobsStateDirectory(), "registry.json");
|
|
1588
|
+
const registryFile = existsSync6(legacyRegistry) ? legacyRegistry : canonicalRegistry;
|
|
1589
|
+
const registry = existsSync6(registryFile) ? readRegistryFile(registryFile) : { version: 1, projects: {} };
|
|
1590
|
+
const registeredProjects = new Set(Object.keys(registry.projects));
|
|
1591
|
+
const projects = new Set([project, ...registeredProjects]);
|
|
1592
|
+
const moves = migrationMoves(projects).filter(({ from }) => existsSync6(from));
|
|
1593
|
+
for (const { from, to } of moves) {
|
|
1594
|
+
if (existsSync6(to)) {
|
|
1595
|
+
throw new Error(`Cannot migrate legacy job storage because both paths exist: ${from} and ${to}. Reconcile or back up one path, then retry; neither path was changed.`);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
for (const { from, to } of moves) {
|
|
1599
|
+
mkdirSync4(path8.dirname(to), { recursive: true });
|
|
1600
|
+
renameSync2(from, to);
|
|
1601
|
+
}
|
|
1602
|
+
for (const workdir of projects)
|
|
1603
|
+
removeLegacyProjectDirectory(workdir);
|
|
1604
|
+
const result = {
|
|
1605
|
+
moved: moves,
|
|
1606
|
+
resyncedProjects: [],
|
|
1607
|
+
warnings: []
|
|
1608
|
+
};
|
|
1609
|
+
if (!shouldResync || moves.length === 0)
|
|
1610
|
+
return result;
|
|
1611
|
+
for (const workdir of registeredProjects) {
|
|
1612
|
+
try {
|
|
1613
|
+
enableProject(workdir);
|
|
1614
|
+
result.resyncedProjects.push(workdir);
|
|
1615
|
+
} catch (error) {
|
|
1616
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1617
|
+
result.warnings.push(`${workdir}: ${message}`);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return result;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1500
1623
|
// src/index.ts
|
|
1501
|
-
var src_default = () => {
|
|
1624
|
+
var src_default = (input) => {
|
|
1625
|
+
if (typeof input.directory === "string") {
|
|
1626
|
+
try {
|
|
1627
|
+
const migration = migrateStorage(input.directory, true);
|
|
1628
|
+
if (migration.moved.length > 0) {
|
|
1629
|
+
console.error(`[opencode-jobs] migrated ${String(migration.moved.length)} legacy storage location(s) to jobs paths`);
|
|
1630
|
+
}
|
|
1631
|
+
for (const warning of migration.warnings) {
|
|
1632
|
+
console.error(`[opencode-jobs] migration warning: ${warning}`);
|
|
1633
|
+
}
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1636
|
+
console.error(`[opencode-jobs] storage migration failed: ${message}`);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1502
1639
|
if (process.platform !== "linux") {
|
|
1503
1640
|
console.error("[opencode-jobs] warning: this is not a Linux host, systemd user timers are unavailable \u2014 scheduling tools will not work here");
|
|
1504
1641
|
}
|
|
1505
|
-
return Promise.resolve({ tool:
|
|
1642
|
+
return Promise.resolve({ tool: jobsTools });
|
|
1506
1643
|
};
|
|
1507
1644
|
export {
|
|
1508
1645
|
src_default as default
|