@caddis/cli 0.4.76 → 0.4.78
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/bundles/antigravity-plugin/guard_agy.py +131 -1
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_temp_prune.py +1 -0
- package/bundles/manifest.json +2 -2
- package/dist/claude-deepseek.js +29 -1
- package/dist/claude-glm.js +29 -1
- package/dist/claude-oss.js +29 -1
- package/dist/cli.js +3051 -372
- package/dist/ui/assets/favicon-DtHQyAC1.svg +8 -0
- package/dist/ui/assets/favicon-dot-DHAVYP15.svg +9 -0
- package/dist/ui/assets/index-AaPwxAoF.js +220 -0
- package/dist/ui/assets/index-DWiypj6N.css +1 -0
- package/dist/ui/assets/mark-small-CwCIyjyC.svg +8 -0
- package/dist/ui/assets/wordmark-CD0s2ofS.svg +99 -0
- package/dist/ui/fonts/OFL-Atkinson.txt +93 -0
- package/dist/ui/fonts/OFL-MapleMono.txt +93 -0
- package/dist/ui/fonts/atkinson-400.849ae6f0.woff2 +0 -0
- package/dist/ui/fonts/atkinson-700.3a9cc18a.woff2 +0 -0
- package/dist/ui/fonts/maple-mono-400.071799fd.woff2 +0 -0
- package/dist/ui/fonts/maple-mono-700.1e17b237.woff2 +0 -0
- package/dist/ui/index.html +14 -0
- package/package.json +13 -3
package/dist/cli.js
CHANGED
|
@@ -51,8 +51,8 @@ var require_error = __commonJS({
|
|
|
51
51
|
* @param {string} code an id string representing the error
|
|
52
52
|
* @param {string} message human-readable description of the error
|
|
53
53
|
*/
|
|
54
|
-
constructor(exitCode, code,
|
|
55
|
-
super(
|
|
54
|
+
constructor(exitCode, code, message3) {
|
|
55
|
+
super(message3);
|
|
56
56
|
Error.captureStackTrace(this, this.constructor);
|
|
57
57
|
this.name = this.constructor.name;
|
|
58
58
|
this.code = code;
|
|
@@ -65,8 +65,8 @@ var require_error = __commonJS({
|
|
|
65
65
|
* Constructs the InvalidArgumentError class
|
|
66
66
|
* @param {string} [message] explanation of why argument is invalid
|
|
67
67
|
*/
|
|
68
|
-
constructor(
|
|
69
|
-
super(1, "commander.invalidArgument",
|
|
68
|
+
constructor(message3) {
|
|
69
|
+
super(1, "commander.invalidArgument", message3);
|
|
70
70
|
Error.captureStackTrace(this, this.constructor);
|
|
71
71
|
this.name = this.constructor.name;
|
|
72
72
|
}
|
|
@@ -639,8 +639,8 @@ var require_help = __commonJS({
|
|
|
639
639
|
* @param {string} str
|
|
640
640
|
* @returns {number}
|
|
641
641
|
*/
|
|
642
|
-
displayWidth(
|
|
643
|
-
return stripColor(
|
|
642
|
+
displayWidth(str3) {
|
|
643
|
+
return stripColor(str3).length;
|
|
644
644
|
}
|
|
645
645
|
/**
|
|
646
646
|
* Style the title for displaying in the help. Called with 'Usage:', 'Options:', etc.
|
|
@@ -648,11 +648,11 @@ var require_help = __commonJS({
|
|
|
648
648
|
* @param {string} str
|
|
649
649
|
* @returns {string}
|
|
650
650
|
*/
|
|
651
|
-
styleTitle(
|
|
652
|
-
return
|
|
651
|
+
styleTitle(str3) {
|
|
652
|
+
return str3;
|
|
653
653
|
}
|
|
654
|
-
styleUsage(
|
|
655
|
-
return
|
|
654
|
+
styleUsage(str3) {
|
|
655
|
+
return str3.split(" ").map((word) => {
|
|
656
656
|
if (word === "[options]") return this.styleOptionText(word);
|
|
657
657
|
if (word === "[command]") return this.styleSubcommandText(word);
|
|
658
658
|
if (word[0] === "[" || word[0] === "<")
|
|
@@ -660,46 +660,46 @@ var require_help = __commonJS({
|
|
|
660
660
|
return this.styleCommandText(word);
|
|
661
661
|
}).join(" ");
|
|
662
662
|
}
|
|
663
|
-
styleCommandDescription(
|
|
664
|
-
return this.styleDescriptionText(
|
|
663
|
+
styleCommandDescription(str3) {
|
|
664
|
+
return this.styleDescriptionText(str3);
|
|
665
665
|
}
|
|
666
|
-
styleOptionDescription(
|
|
667
|
-
return this.styleDescriptionText(
|
|
666
|
+
styleOptionDescription(str3) {
|
|
667
|
+
return this.styleDescriptionText(str3);
|
|
668
668
|
}
|
|
669
|
-
styleSubcommandDescription(
|
|
670
|
-
return this.styleDescriptionText(
|
|
669
|
+
styleSubcommandDescription(str3) {
|
|
670
|
+
return this.styleDescriptionText(str3);
|
|
671
671
|
}
|
|
672
|
-
styleArgumentDescription(
|
|
673
|
-
return this.styleDescriptionText(
|
|
672
|
+
styleArgumentDescription(str3) {
|
|
673
|
+
return this.styleDescriptionText(str3);
|
|
674
674
|
}
|
|
675
|
-
styleDescriptionText(
|
|
676
|
-
return
|
|
675
|
+
styleDescriptionText(str3) {
|
|
676
|
+
return str3;
|
|
677
677
|
}
|
|
678
|
-
styleOptionTerm(
|
|
679
|
-
return this.styleOptionText(
|
|
678
|
+
styleOptionTerm(str3) {
|
|
679
|
+
return this.styleOptionText(str3);
|
|
680
680
|
}
|
|
681
|
-
styleSubcommandTerm(
|
|
682
|
-
return
|
|
681
|
+
styleSubcommandTerm(str3) {
|
|
682
|
+
return str3.split(" ").map((word) => {
|
|
683
683
|
if (word === "[options]") return this.styleOptionText(word);
|
|
684
684
|
if (word[0] === "[" || word[0] === "<")
|
|
685
685
|
return this.styleArgumentText(word);
|
|
686
686
|
return this.styleSubcommandText(word);
|
|
687
687
|
}).join(" ");
|
|
688
688
|
}
|
|
689
|
-
styleArgumentTerm(
|
|
690
|
-
return this.styleArgumentText(
|
|
689
|
+
styleArgumentTerm(str3) {
|
|
690
|
+
return this.styleArgumentText(str3);
|
|
691
691
|
}
|
|
692
|
-
styleOptionText(
|
|
693
|
-
return
|
|
692
|
+
styleOptionText(str3) {
|
|
693
|
+
return str3;
|
|
694
694
|
}
|
|
695
|
-
styleArgumentText(
|
|
696
|
-
return
|
|
695
|
+
styleArgumentText(str3) {
|
|
696
|
+
return str3;
|
|
697
697
|
}
|
|
698
|
-
styleSubcommandText(
|
|
699
|
-
return
|
|
698
|
+
styleSubcommandText(str3) {
|
|
699
|
+
return str3;
|
|
700
700
|
}
|
|
701
|
-
styleCommandText(
|
|
702
|
-
return
|
|
701
|
+
styleCommandText(str3) {
|
|
702
|
+
return str3;
|
|
703
703
|
}
|
|
704
704
|
/**
|
|
705
705
|
* Calculate the pad width from the maximum term length.
|
|
@@ -722,8 +722,8 @@ var require_help = __commonJS({
|
|
|
722
722
|
* @param {string} str
|
|
723
723
|
* @returns {boolean}
|
|
724
724
|
*/
|
|
725
|
-
preformatted(
|
|
726
|
-
return /\n[^\S\r\n]/.test(
|
|
725
|
+
preformatted(str3) {
|
|
726
|
+
return /\n[^\S\r\n]/.test(str3);
|
|
727
727
|
}
|
|
728
728
|
/**
|
|
729
729
|
* Format the "item", which consists of a term and description. Pad the term and wrap the description, indenting the following lines.
|
|
@@ -769,9 +769,9 @@ ${itemIndentStr}`);
|
|
|
769
769
|
* @param {number} width
|
|
770
770
|
* @returns {string}
|
|
771
771
|
*/
|
|
772
|
-
boxWrap(
|
|
773
|
-
if (width < this.minWidthToWrap) return
|
|
774
|
-
const rawLines =
|
|
772
|
+
boxWrap(str3, width) {
|
|
773
|
+
if (width < this.minWidthToWrap) return str3;
|
|
774
|
+
const rawLines = str3.split(/\r\n|\n/);
|
|
775
775
|
const chunkPattern = /[\s]*[^\s]+/g;
|
|
776
776
|
const wrappedLines = [];
|
|
777
777
|
rawLines.forEach((line2) => {
|
|
@@ -799,9 +799,9 @@ ${itemIndentStr}`);
|
|
|
799
799
|
return wrappedLines.join("\n");
|
|
800
800
|
}
|
|
801
801
|
};
|
|
802
|
-
function stripColor(
|
|
802
|
+
function stripColor(str3) {
|
|
803
803
|
const sgrPattern = /\x1b\[\d*(;\d*)*m/g;
|
|
804
|
-
return
|
|
804
|
+
return str3.replace(sgrPattern, "");
|
|
805
805
|
}
|
|
806
806
|
exports.Help = Help2;
|
|
807
807
|
exports.stripColor = stripColor;
|
|
@@ -1073,9 +1073,9 @@ var require_option = __commonJS({
|
|
|
1073
1073
|
return option.negate === (negativeValue === value);
|
|
1074
1074
|
}
|
|
1075
1075
|
};
|
|
1076
|
-
function camelcase(
|
|
1077
|
-
return
|
|
1078
|
-
return
|
|
1076
|
+
function camelcase(str3) {
|
|
1077
|
+
return str3.split("-").reduce((str4, word) => {
|
|
1078
|
+
return str4 + word[0].toUpperCase() + word.slice(1);
|
|
1079
1079
|
});
|
|
1080
1080
|
}
|
|
1081
1081
|
function splitOptionFlags(flags) {
|
|
@@ -1209,7 +1209,7 @@ var require_command = __commonJS({
|
|
|
1209
1209
|
"use strict";
|
|
1210
1210
|
var EventEmitter = __require("events").EventEmitter;
|
|
1211
1211
|
var childProcess = __require("child_process");
|
|
1212
|
-
var
|
|
1212
|
+
var path20 = __require("path");
|
|
1213
1213
|
var fs = __require("fs");
|
|
1214
1214
|
var process3 = __require("process");
|
|
1215
1215
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
@@ -1258,14 +1258,14 @@ var require_command = __commonJS({
|
|
|
1258
1258
|
this._showSuggestionAfterError = true;
|
|
1259
1259
|
this._savedState = null;
|
|
1260
1260
|
this._outputConfiguration = {
|
|
1261
|
-
writeOut: (
|
|
1262
|
-
writeErr: (
|
|
1263
|
-
outputError: (
|
|
1261
|
+
writeOut: (str3) => process3.stdout.write(str3),
|
|
1262
|
+
writeErr: (str3) => process3.stderr.write(str3),
|
|
1263
|
+
outputError: (str3, write) => write(str3),
|
|
1264
1264
|
getOutHelpWidth: () => process3.stdout.isTTY ? process3.stdout.columns : void 0,
|
|
1265
1265
|
getErrHelpWidth: () => process3.stderr.isTTY ? process3.stderr.columns : void 0,
|
|
1266
1266
|
getOutHasColors: () => useColor() ?? (process3.stdout.isTTY && process3.stdout.hasColors?.()),
|
|
1267
1267
|
getErrHasColors: () => useColor() ?? (process3.stderr.isTTY && process3.stderr.hasColors?.()),
|
|
1268
|
-
stripColor: (
|
|
1268
|
+
stripColor: (str3) => stripColor(str3)
|
|
1269
1269
|
};
|
|
1270
1270
|
this._hidden = false;
|
|
1271
1271
|
this._helpOption = void 0;
|
|
@@ -1654,9 +1654,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1654
1654
|
* @return never
|
|
1655
1655
|
* @private
|
|
1656
1656
|
*/
|
|
1657
|
-
_exit(exitCode, code,
|
|
1657
|
+
_exit(exitCode, code, message3) {
|
|
1658
1658
|
if (this._exitCallback) {
|
|
1659
|
-
this._exitCallback(new CommanderError2(exitCode, code,
|
|
1659
|
+
this._exitCallback(new CommanderError2(exitCode, code, message3));
|
|
1660
1660
|
}
|
|
1661
1661
|
process3.exit(exitCode);
|
|
1662
1662
|
}
|
|
@@ -1716,8 +1716,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1716
1716
|
return target.parseArg(value, previous);
|
|
1717
1717
|
} catch (err) {
|
|
1718
1718
|
if (err.code === "commander.invalidArgument") {
|
|
1719
|
-
const
|
|
1720
|
-
this.error(
|
|
1719
|
+
const message3 = `${invalidArgumentMessage} ${err.message}`;
|
|
1720
|
+
this.error(message3, { exitCode: err.exitCode, code: err.code });
|
|
1721
1721
|
}
|
|
1722
1722
|
throw err;
|
|
1723
1723
|
}
|
|
@@ -2222,9 +2222,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2222
2222
|
let launchWithNode = false;
|
|
2223
2223
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
2224
2224
|
function findFile(baseDir, baseName) {
|
|
2225
|
-
const localBin =
|
|
2225
|
+
const localBin = path20.resolve(baseDir, baseName);
|
|
2226
2226
|
if (fs.existsSync(localBin)) return localBin;
|
|
2227
|
-
if (sourceExt.includes(
|
|
2227
|
+
if (sourceExt.includes(path20.extname(baseName))) return void 0;
|
|
2228
2228
|
const foundExt = sourceExt.find(
|
|
2229
2229
|
(ext) => fs.existsSync(`${localBin}${ext}`)
|
|
2230
2230
|
);
|
|
@@ -2242,17 +2242,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2242
2242
|
} catch {
|
|
2243
2243
|
resolvedScriptPath = this._scriptPath;
|
|
2244
2244
|
}
|
|
2245
|
-
executableDir =
|
|
2246
|
-
|
|
2245
|
+
executableDir = path20.resolve(
|
|
2246
|
+
path20.dirname(resolvedScriptPath),
|
|
2247
2247
|
executableDir
|
|
2248
2248
|
);
|
|
2249
2249
|
}
|
|
2250
2250
|
if (executableDir) {
|
|
2251
2251
|
let localFile = findFile(executableDir, executableFile);
|
|
2252
2252
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
2253
|
-
const legacyName =
|
|
2253
|
+
const legacyName = path20.basename(
|
|
2254
2254
|
this._scriptPath,
|
|
2255
|
-
|
|
2255
|
+
path20.extname(this._scriptPath)
|
|
2256
2256
|
);
|
|
2257
2257
|
if (legacyName !== this._name) {
|
|
2258
2258
|
localFile = findFile(
|
|
@@ -2263,7 +2263,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2263
2263
|
}
|
|
2264
2264
|
executableFile = localFile || executableFile;
|
|
2265
2265
|
}
|
|
2266
|
-
launchWithNode = sourceExt.includes(
|
|
2266
|
+
launchWithNode = sourceExt.includes(path20.extname(executableFile));
|
|
2267
2267
|
let proc;
|
|
2268
2268
|
if (process3.platform !== "win32") {
|
|
2269
2269
|
if (launchWithNode) {
|
|
@@ -2793,9 +2793,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2793
2793
|
* @param {string} [errorOptions.code] - an id string representing the error
|
|
2794
2794
|
* @param {number} [errorOptions.exitCode] - used with process.exit
|
|
2795
2795
|
*/
|
|
2796
|
-
error(
|
|
2796
|
+
error(message3, errorOptions) {
|
|
2797
2797
|
this._outputConfiguration.outputError(
|
|
2798
|
-
`${
|
|
2798
|
+
`${message3}
|
|
2799
2799
|
`,
|
|
2800
2800
|
this._outputConfiguration.writeErr
|
|
2801
2801
|
);
|
|
@@ -2809,7 +2809,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2809
2809
|
const config = errorOptions || {};
|
|
2810
2810
|
const exitCode = config.exitCode || 1;
|
|
2811
2811
|
const code = config.code || "commander.error";
|
|
2812
|
-
this._exit(exitCode, code,
|
|
2812
|
+
this._exit(exitCode, code, message3);
|
|
2813
2813
|
}
|
|
2814
2814
|
/**
|
|
2815
2815
|
* Apply any option related environment variables, if option does
|
|
@@ -2865,8 +2865,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2865
2865
|
* @private
|
|
2866
2866
|
*/
|
|
2867
2867
|
missingArgument(name) {
|
|
2868
|
-
const
|
|
2869
|
-
this.error(
|
|
2868
|
+
const message3 = `error: missing required argument '${name}'`;
|
|
2869
|
+
this.error(message3, { code: "commander.missingArgument" });
|
|
2870
2870
|
}
|
|
2871
2871
|
/**
|
|
2872
2872
|
* `Option` is missing an argument.
|
|
@@ -2875,8 +2875,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2875
2875
|
* @private
|
|
2876
2876
|
*/
|
|
2877
2877
|
optionMissingArgument(option) {
|
|
2878
|
-
const
|
|
2879
|
-
this.error(
|
|
2878
|
+
const message3 = `error: option '${option.flags}' argument missing`;
|
|
2879
|
+
this.error(message3, { code: "commander.optionMissingArgument" });
|
|
2880
2880
|
}
|
|
2881
2881
|
/**
|
|
2882
2882
|
* `Option` does not have a value, and is a mandatory option.
|
|
@@ -2885,8 +2885,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2885
2885
|
* @private
|
|
2886
2886
|
*/
|
|
2887
2887
|
missingMandatoryOptionValue(option) {
|
|
2888
|
-
const
|
|
2889
|
-
this.error(
|
|
2888
|
+
const message3 = `error: required option '${option.flags}' not specified`;
|
|
2889
|
+
this.error(message3, { code: "commander.missingMandatoryOptionValue" });
|
|
2890
2890
|
}
|
|
2891
2891
|
/**
|
|
2892
2892
|
* `Option` conflicts with another option.
|
|
@@ -2919,8 +2919,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2919
2919
|
}
|
|
2920
2920
|
return `option '${bestOption.flags}'`;
|
|
2921
2921
|
};
|
|
2922
|
-
const
|
|
2923
|
-
this.error(
|
|
2922
|
+
const message3 = `error: ${getErrorMessage(option)} cannot be used with ${getErrorMessage(conflictingOption)}`;
|
|
2923
|
+
this.error(message3, { code: "commander.conflictingOption" });
|
|
2924
2924
|
}
|
|
2925
2925
|
/**
|
|
2926
2926
|
* Unknown option `flag`.
|
|
@@ -2941,8 +2941,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2941
2941
|
} while (command && !command._enablePositionalOptions);
|
|
2942
2942
|
suggestion = suggestSimilar(flag, candidateFlags);
|
|
2943
2943
|
}
|
|
2944
|
-
const
|
|
2945
|
-
this.error(
|
|
2944
|
+
const message3 = `error: unknown option '${flag}'${suggestion}`;
|
|
2945
|
+
this.error(message3, { code: "commander.unknownOption" });
|
|
2946
2946
|
}
|
|
2947
2947
|
/**
|
|
2948
2948
|
* Excess arguments, more than expected.
|
|
@@ -2955,8 +2955,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2955
2955
|
const expected = this.registeredArguments.length;
|
|
2956
2956
|
const s = expected === 1 ? "" : "s";
|
|
2957
2957
|
const forSubcommand = this.parent ? ` for '${this.name()}'` : "";
|
|
2958
|
-
const
|
|
2959
|
-
this.error(
|
|
2958
|
+
const message3 = `error: too many arguments${forSubcommand}. Expected ${expected} argument${s} but got ${receivedArgs.length}.`;
|
|
2959
|
+
this.error(message3, { code: "commander.excessArguments" });
|
|
2960
2960
|
}
|
|
2961
2961
|
/**
|
|
2962
2962
|
* Unknown command.
|
|
@@ -2974,8 +2974,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2974
2974
|
});
|
|
2975
2975
|
suggestion = suggestSimilar(unknownName, candidateNames);
|
|
2976
2976
|
}
|
|
2977
|
-
const
|
|
2978
|
-
this.error(
|
|
2977
|
+
const message3 = `error: unknown command '${unknownName}'${suggestion}`;
|
|
2978
|
+
this.error(message3, { code: "commander.unknownCommand" });
|
|
2979
2979
|
}
|
|
2980
2980
|
/**
|
|
2981
2981
|
* Get or set the program version.
|
|
@@ -2989,18 +2989,18 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2989
2989
|
* @param {string} [description]
|
|
2990
2990
|
* @return {(this | string | undefined)} `this` command for chaining, or version string if no arguments
|
|
2991
2991
|
*/
|
|
2992
|
-
version(
|
|
2993
|
-
if (
|
|
2994
|
-
this._version =
|
|
2992
|
+
version(str3, flags, description) {
|
|
2993
|
+
if (str3 === void 0) return this._version;
|
|
2994
|
+
this._version = str3;
|
|
2995
2995
|
flags = flags || "-V, --version";
|
|
2996
2996
|
description = description || "output the version number";
|
|
2997
2997
|
const versionOption = this.createOption(flags, description);
|
|
2998
2998
|
this._versionOptionName = versionOption.attributeName();
|
|
2999
2999
|
this._registerOption(versionOption);
|
|
3000
3000
|
this.on("option:" + versionOption.name(), () => {
|
|
3001
|
-
this._outputConfiguration.writeOut(`${
|
|
3001
|
+
this._outputConfiguration.writeOut(`${str3}
|
|
3002
3002
|
`);
|
|
3003
|
-
this._exit(0, "commander.version",
|
|
3003
|
+
this._exit(0, "commander.version", str3);
|
|
3004
3004
|
});
|
|
3005
3005
|
return this;
|
|
3006
3006
|
}
|
|
@@ -3011,10 +3011,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3011
3011
|
* @param {object} [argsDescription]
|
|
3012
3012
|
* @return {(string|Command)}
|
|
3013
3013
|
*/
|
|
3014
|
-
description(
|
|
3015
|
-
if (
|
|
3014
|
+
description(str3, argsDescription) {
|
|
3015
|
+
if (str3 === void 0 && argsDescription === void 0)
|
|
3016
3016
|
return this._description;
|
|
3017
|
-
this._description =
|
|
3017
|
+
this._description = str3;
|
|
3018
3018
|
if (argsDescription) {
|
|
3019
3019
|
this._argsDescription = argsDescription;
|
|
3020
3020
|
}
|
|
@@ -3026,9 +3026,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3026
3026
|
* @param {string} [str]
|
|
3027
3027
|
* @return {(string|Command)}
|
|
3028
3028
|
*/
|
|
3029
|
-
summary(
|
|
3030
|
-
if (
|
|
3031
|
-
this._summary =
|
|
3029
|
+
summary(str3) {
|
|
3030
|
+
if (str3 === void 0) return this._summary;
|
|
3031
|
+
this._summary = str3;
|
|
3032
3032
|
return this;
|
|
3033
3033
|
}
|
|
3034
3034
|
/**
|
|
@@ -3076,8 +3076,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3076
3076
|
* @param {string} [str]
|
|
3077
3077
|
* @return {(string|Command)}
|
|
3078
3078
|
*/
|
|
3079
|
-
usage(
|
|
3080
|
-
if (
|
|
3079
|
+
usage(str3) {
|
|
3080
|
+
if (str3 === void 0) {
|
|
3081
3081
|
if (this._usage) return this._usage;
|
|
3082
3082
|
const args = this.registeredArguments.map((arg) => {
|
|
3083
3083
|
return humanReadableArgName(arg);
|
|
@@ -3088,7 +3088,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3088
3088
|
this.registeredArguments.length ? args : []
|
|
3089
3089
|
).join(" ");
|
|
3090
3090
|
}
|
|
3091
|
-
this._usage =
|
|
3091
|
+
this._usage = str3;
|
|
3092
3092
|
return this;
|
|
3093
3093
|
}
|
|
3094
3094
|
/**
|
|
@@ -3097,9 +3097,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3097
3097
|
* @param {string} [str]
|
|
3098
3098
|
* @return {(string|Command)}
|
|
3099
3099
|
*/
|
|
3100
|
-
name(
|
|
3101
|
-
if (
|
|
3102
|
-
this._name =
|
|
3100
|
+
name(str3) {
|
|
3101
|
+
if (str3 === void 0) return this._name;
|
|
3102
|
+
this._name = str3;
|
|
3103
3103
|
return this;
|
|
3104
3104
|
}
|
|
3105
3105
|
/**
|
|
@@ -3178,7 +3178,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3178
3178
|
* @return {Command}
|
|
3179
3179
|
*/
|
|
3180
3180
|
nameFromFilename(filename) {
|
|
3181
|
-
this._name =
|
|
3181
|
+
this._name = path20.basename(filename, path20.extname(filename));
|
|
3182
3182
|
return this;
|
|
3183
3183
|
}
|
|
3184
3184
|
/**
|
|
@@ -3192,9 +3192,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3192
3192
|
* @param {string} [path]
|
|
3193
3193
|
* @return {(string|null|Command)}
|
|
3194
3194
|
*/
|
|
3195
|
-
executableDir(
|
|
3196
|
-
if (
|
|
3197
|
-
this._executableDir =
|
|
3195
|
+
executableDir(path21) {
|
|
3196
|
+
if (path21 === void 0) return this._executableDir;
|
|
3197
|
+
this._executableDir = path21;
|
|
3198
3198
|
return this;
|
|
3199
3199
|
}
|
|
3200
3200
|
/**
|
|
@@ -3233,17 +3233,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3233
3233
|
let hasColors;
|
|
3234
3234
|
let helpWidth;
|
|
3235
3235
|
if (error) {
|
|
3236
|
-
baseWrite = (
|
|
3236
|
+
baseWrite = (str3) => this._outputConfiguration.writeErr(str3);
|
|
3237
3237
|
hasColors = this._outputConfiguration.getErrHasColors();
|
|
3238
3238
|
helpWidth = this._outputConfiguration.getErrHelpWidth();
|
|
3239
3239
|
} else {
|
|
3240
|
-
baseWrite = (
|
|
3240
|
+
baseWrite = (str3) => this._outputConfiguration.writeOut(str3);
|
|
3241
3241
|
hasColors = this._outputConfiguration.getOutHasColors();
|
|
3242
3242
|
helpWidth = this._outputConfiguration.getOutHelpWidth();
|
|
3243
3243
|
}
|
|
3244
|
-
const write = (
|
|
3245
|
-
if (!hasColors)
|
|
3246
|
-
return baseWrite(
|
|
3244
|
+
const write = (str3) => {
|
|
3245
|
+
if (!hasColors) str3 = this._outputConfiguration.stripColor(str3);
|
|
3246
|
+
return baseWrite(str3);
|
|
3247
3247
|
};
|
|
3248
3248
|
return { error, write, hasColors, helpWidth };
|
|
3249
3249
|
}
|
|
@@ -3927,12 +3927,28 @@ var init_theme = __esm({
|
|
|
3927
3927
|
}
|
|
3928
3928
|
});
|
|
3929
3929
|
|
|
3930
|
+
// ui/src/brand/palette.ts
|
|
3931
|
+
var PALETTE;
|
|
3932
|
+
var init_palette = __esm({
|
|
3933
|
+
"ui/src/brand/palette.ts"() {
|
|
3934
|
+
"use strict";
|
|
3935
|
+
PALETTE = {
|
|
3936
|
+
blue: "#0D42FC",
|
|
3937
|
+
cyan: "#00CAD5",
|
|
3938
|
+
green: "#91D202",
|
|
3939
|
+
amber: "#FDAD01",
|
|
3940
|
+
orange: "#FC4E23",
|
|
3941
|
+
violet: "#8730FB"
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
});
|
|
3945
|
+
|
|
3930
3946
|
// src/util/banner.ts
|
|
3931
3947
|
function renderBanner(options = {}) {
|
|
3932
3948
|
process.stdout.write("\n");
|
|
3933
3949
|
for (let i = 0; i < 7; i++) {
|
|
3934
|
-
const m2 = import_picocolors2.default.bold(
|
|
3935
|
-
const w2 = i < 2 ? import_picocolors2.default.bold(
|
|
3950
|
+
const m2 = import_picocolors2.default.bold(color.mark(MARK[i]));
|
|
3951
|
+
const w2 = i < 2 ? import_picocolors2.default.bold(color.top(WORDMARK[i])) : i < 4 ? import_picocolors2.default.bold(color.mid(WORDMARK[i])) : import_picocolors2.default.bold(color.bottom(WORDMARK[i]));
|
|
3936
3952
|
process.stdout.write(` ${m2} ${w2}
|
|
3937
3953
|
`);
|
|
3938
3954
|
}
|
|
@@ -3941,19 +3957,36 @@ function renderBanner(options = {}) {
|
|
|
3941
3957
|
`);
|
|
3942
3958
|
if (options.cliVersion || options.poolVersion) {
|
|
3943
3959
|
const parts = [];
|
|
3944
|
-
if (options.cliVersion) parts.push(`cli ${
|
|
3945
|
-
if (options.poolVersion) parts.push(`pool ${
|
|
3960
|
+
if (options.cliVersion) parts.push(`cli ${color.cli(options.cliVersion)}`);
|
|
3961
|
+
if (options.poolVersion) parts.push(`pool ${color.pool(import_picocolors2.default.bold(options.poolVersion))}`);
|
|
3946
3962
|
process.stdout.write(` ${import_picocolors2.default.dim(parts.join(" \xB7 "))}
|
|
3947
3963
|
`);
|
|
3948
3964
|
}
|
|
3949
3965
|
process.stdout.write("\n");
|
|
3950
3966
|
}
|
|
3951
|
-
var import_picocolors2, MARK, WORDMARK;
|
|
3967
|
+
var import_picocolors2, BANNER_COLORS, color, MARK, WORDMARK;
|
|
3952
3968
|
var init_banner = __esm({
|
|
3953
3969
|
"src/util/banner.ts"() {
|
|
3954
3970
|
"use strict";
|
|
3955
3971
|
import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
3956
3972
|
init_theme();
|
|
3973
|
+
init_palette();
|
|
3974
|
+
BANNER_COLORS = {
|
|
3975
|
+
mark: PALETTE.green,
|
|
3976
|
+
wordmarkTop: PALETTE.cyan,
|
|
3977
|
+
wordmarkMid: PALETTE.green,
|
|
3978
|
+
wordmarkBottom: PALETTE.violet,
|
|
3979
|
+
cliVersion: PALETTE.cyan,
|
|
3980
|
+
poolVersion: PALETTE.violet
|
|
3981
|
+
};
|
|
3982
|
+
color = {
|
|
3983
|
+
mark: hex(BANNER_COLORS.mark),
|
|
3984
|
+
top: hex(BANNER_COLORS.wordmarkTop),
|
|
3985
|
+
mid: hex(BANNER_COLORS.wordmarkMid),
|
|
3986
|
+
bottom: hex(BANNER_COLORS.wordmarkBottom),
|
|
3987
|
+
cli: hex(BANNER_COLORS.cliVersion),
|
|
3988
|
+
pool: hex(BANNER_COLORS.poolVersion)
|
|
3989
|
+
};
|
|
3957
3990
|
MARK = [
|
|
3958
3991
|
" \u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2584 ",
|
|
3959
3992
|
" \u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584 ",
|
|
@@ -4001,7 +4034,7 @@ function errorLine(text) {
|
|
|
4001
4034
|
process.stderr.write(`${theme.coral("error")} ${text}
|
|
4002
4035
|
`);
|
|
4003
4036
|
}
|
|
4004
|
-
var import_picocolors3, OK, WARN, FAIL, SKIP, MARKS,
|
|
4037
|
+
var import_picocolors3, OK, WARN, FAIL, SKIP, MARKS, color2;
|
|
4005
4038
|
var init_log = __esm({
|
|
4006
4039
|
"src/util/log.ts"() {
|
|
4007
4040
|
"use strict";
|
|
@@ -4020,7 +4053,7 @@ var init_log = __esm({
|
|
|
4020
4053
|
skip: import_picocolors3.default.dim("\xB7"),
|
|
4021
4054
|
info: theme.sky("\u2022")
|
|
4022
4055
|
};
|
|
4023
|
-
|
|
4056
|
+
color2 = Object.assign(import_picocolors3.default, {
|
|
4024
4057
|
mint: theme.mint,
|
|
4025
4058
|
violet: theme.violet,
|
|
4026
4059
|
magenta: theme.magenta,
|
|
@@ -4641,14 +4674,14 @@ async function driveAgents(entries, options) {
|
|
|
4641
4674
|
}
|
|
4642
4675
|
if (result.ok) {
|
|
4643
4676
|
outcome.changed += 1;
|
|
4644
|
-
spinner?.stop(`${label} \u2014 ${
|
|
4677
|
+
spinner?.stop(`${label} \u2014 ${color2.green("done")}`, 0);
|
|
4645
4678
|
if (!spinner) item("ok", `${label} \u2014 done`);
|
|
4646
4679
|
for (const step of result.steps) detail(step.command);
|
|
4647
4680
|
if (result.message) detail(result.message);
|
|
4648
4681
|
continue;
|
|
4649
4682
|
}
|
|
4650
4683
|
outcome.failed += 1;
|
|
4651
|
-
spinner?.stop(`${label} \u2014 ${
|
|
4684
|
+
spinner?.stop(`${label} \u2014 ${color2.red("failed")}`, 1);
|
|
4652
4685
|
if (!spinner) item("fail", `${label} \u2014 failed`);
|
|
4653
4686
|
if (result.message) detail(result.message);
|
|
4654
4687
|
for (const step of result.steps) {
|
|
@@ -4664,18 +4697,18 @@ async function driveAgents(entries, options) {
|
|
|
4664
4697
|
function reportOutcome(outcome, dryRun) {
|
|
4665
4698
|
line("");
|
|
4666
4699
|
if (dryRun) {
|
|
4667
|
-
line(` ${
|
|
4700
|
+
line(` ${color2.cyan("dry run")} \u2014 nothing was changed. Re-run without --dry-run to apply.`);
|
|
4668
4701
|
return 0;
|
|
4669
4702
|
}
|
|
4670
4703
|
if (outcome.failed > 0) {
|
|
4671
|
-
line(` ${
|
|
4704
|
+
line(` ${color2.red(`${outcome.failed} agent(s) failed`)}, ${outcome.changed} updated. Run ${color2.cyan("caddis doctor")} for detail.`);
|
|
4672
4705
|
return 1;
|
|
4673
4706
|
}
|
|
4674
4707
|
if (outcome.changed === 0) {
|
|
4675
|
-
line(` ${
|
|
4708
|
+
line(` ${color2.green("Nothing to do")} \u2014 everything already current.`);
|
|
4676
4709
|
return 0;
|
|
4677
4710
|
}
|
|
4678
|
-
line(` ${
|
|
4711
|
+
line(` ${color2.green(`${outcome.changed} agent(s) updated.`)} Run ${color2.cyan("caddis status")} to confirm.`);
|
|
4679
4712
|
return 0;
|
|
4680
4713
|
}
|
|
4681
4714
|
var init_drive = __esm({
|
|
@@ -4793,35 +4826,35 @@ async function init(options) {
|
|
|
4793
4826
|
const report = await gather(options.adapters);
|
|
4794
4827
|
const interactive = !options.yes && !options.dryRun && process.stdout.isTTY === true;
|
|
4795
4828
|
renderBanner({ cliVersion: report.cliVersion, poolVersion: report.poolVersion });
|
|
4796
|
-
Ie(`${
|
|
4829
|
+
Ie(`${color2.bold("caddis installer")} ${color2.dim(`\xB7 pool ${report.poolVersion}`)}`);
|
|
4797
4830
|
heading("Detected Agents");
|
|
4798
4831
|
for (const entry of report.agents) {
|
|
4799
4832
|
if (!entry.detection.present) {
|
|
4800
|
-
item("skip", `${entry.adapter.name} \u2014 ${
|
|
4833
|
+
item("skip", `${entry.adapter.name} \u2014 ${color2.dim("not found")}`);
|
|
4801
4834
|
continue;
|
|
4802
4835
|
}
|
|
4803
4836
|
if (!entry.adapter.supported) {
|
|
4804
|
-
item("info", `${entry.adapter.name} \u2014 ${
|
|
4837
|
+
item("info", `${entry.adapter.name} \u2014 ${color2.dim("detected, not yet supported (v0.2)")}`);
|
|
4805
4838
|
continue;
|
|
4806
4839
|
}
|
|
4807
4840
|
const current = entry.drift === "current";
|
|
4808
4841
|
item(
|
|
4809
4842
|
current ? "ok" : "warn",
|
|
4810
|
-
`${entry.adapter.name} \u2014 ${current ? `caddis ${entry.status.version} ${
|
|
4843
|
+
`${entry.adapter.name} \u2014 ${current ? `caddis ${entry.status.version} ${color2.dim("(current)")}` : entry.status.installed ? `caddis ${entry.status.version ?? "?"} \u2192 ${color2.bold(report.poolVersion)}` : color2.amber("caddis not installed")}`
|
|
4811
4844
|
);
|
|
4812
4845
|
}
|
|
4813
4846
|
const targets = actionable(report, { includeCurrent: true });
|
|
4814
4847
|
if (targets.length === 0) {
|
|
4815
4848
|
Se(
|
|
4816
|
-
`${
|
|
4849
|
+
`${color2.yellow("No supported agent found.")} caddis v0.1 drives Claude Code and agy \u2014 install one, then re-run.`
|
|
4817
4850
|
);
|
|
4818
4851
|
return 0;
|
|
4819
4852
|
}
|
|
4820
4853
|
line("");
|
|
4821
4854
|
heading("Planned Installations");
|
|
4822
4855
|
for (const entry of targets) {
|
|
4823
|
-
line(` ${
|
|
4824
|
-
line(` ${
|
|
4856
|
+
line(` ${color2.mint("\u2022")} ${color2.bold(entry.adapter.name)}`);
|
|
4857
|
+
line(` ${color2.dim("\u2192")} ${entry.adapter.summary}`);
|
|
4825
4858
|
}
|
|
4826
4859
|
line("");
|
|
4827
4860
|
if (interactive) {
|
|
@@ -4843,11 +4876,11 @@ async function init(options) {
|
|
|
4843
4876
|
const code = reportOutcome(outcome, options.dryRun);
|
|
4844
4877
|
if (code === 0 && !options.dryRun) {
|
|
4845
4878
|
line("");
|
|
4846
|
-
line(` ${
|
|
4847
|
-
line(` ${
|
|
4879
|
+
line(` ${color2.mint("\u2713")} ${color2.bold("Caddis dev harness is ready.")} Open any configured agent above to start.`);
|
|
4880
|
+
line(` ${color2.dim("Run")} ${color2.sky("caddis doctor")} ${color2.dim("at any time to verify agent health and updates.")}`);
|
|
4848
4881
|
line("");
|
|
4849
4882
|
}
|
|
4850
|
-
Se(code === 0 ?
|
|
4883
|
+
Se(code === 0 ? color2.green("Setup complete.") : color2.red("Finished with errors."));
|
|
4851
4884
|
return code;
|
|
4852
4885
|
}
|
|
4853
4886
|
var init_init = __esm({
|
|
@@ -4874,10 +4907,10 @@ function keysFilePath(env) {
|
|
|
4874
4907
|
const expanded = expandHome(configured);
|
|
4875
4908
|
return isAbsolute(expanded) ? expanded : expanded;
|
|
4876
4909
|
}
|
|
4877
|
-
function parseKeysFile(
|
|
4910
|
+
function parseKeysFile(path20) {
|
|
4878
4911
|
let text;
|
|
4879
4912
|
try {
|
|
4880
|
-
text = readFileSync3(
|
|
4913
|
+
text = readFileSync3(path20, "utf8");
|
|
4881
4914
|
} catch {
|
|
4882
4915
|
return {};
|
|
4883
4916
|
}
|
|
@@ -4959,8 +4992,8 @@ async function probeKey(baseUrl, model, apiKey) {
|
|
|
4959
4992
|
}
|
|
4960
4993
|
return { ok: false, rejected: false, detail: `could not validate (HTTP ${res.status})` };
|
|
4961
4994
|
} catch (error) {
|
|
4962
|
-
const
|
|
4963
|
-
return { ok: false, rejected: false, detail: `could not validate (${
|
|
4995
|
+
const message3 = error instanceof Error ? error.message : String(error);
|
|
4996
|
+
return { ok: false, rejected: false, detail: `could not validate (${message3})` };
|
|
4964
4997
|
} finally {
|
|
4965
4998
|
clearTimeout(timer);
|
|
4966
4999
|
}
|
|
@@ -4989,18 +5022,18 @@ function upsertKeyLines(existing, updates) {
|
|
|
4989
5022
|
if (!text.endsWith("\n")) text += "\n";
|
|
4990
5023
|
return text;
|
|
4991
5024
|
}
|
|
4992
|
-
function restrictPermissions(
|
|
5025
|
+
function restrictPermissions(path20) {
|
|
4993
5026
|
if (process.platform === "win32") return null;
|
|
4994
5027
|
try {
|
|
4995
|
-
chmodSync(
|
|
5028
|
+
chmodSync(path20, 384);
|
|
4996
5029
|
return null;
|
|
4997
5030
|
} catch (error) {
|
|
4998
5031
|
return error instanceof Error ? error.message : String(error);
|
|
4999
5032
|
}
|
|
5000
5033
|
}
|
|
5001
5034
|
async function keys(options = {}) {
|
|
5002
|
-
const
|
|
5003
|
-
const fileKeys = parseKeysFile(
|
|
5035
|
+
const path20 = keysFilePath(process.env);
|
|
5036
|
+
const fileKeys = parseKeysFile(path20);
|
|
5004
5037
|
const names = Object.keys(PROVIDERS).sort();
|
|
5005
5038
|
const current = names.map((name) => {
|
|
5006
5039
|
const preset = PROVIDERS[name];
|
|
@@ -5016,14 +5049,14 @@ async function keys(options = {}) {
|
|
|
5016
5049
|
if (options.check) {
|
|
5017
5050
|
const configured = current.filter((entry) => entry.value);
|
|
5018
5051
|
if (!configured.length) {
|
|
5019
|
-
process.stdout.write(`no provider keys configured (looked in $ENV and ${
|
|
5052
|
+
process.stdout.write(`no provider keys configured (looked in $ENV and ${path20})
|
|
5020
5053
|
`);
|
|
5021
5054
|
return 1;
|
|
5022
5055
|
}
|
|
5023
5056
|
let bad = 0;
|
|
5024
5057
|
for (const entry of configured) {
|
|
5025
5058
|
const probe = await probeKey(entry.preset.baseUrl, entry.preset.model, entry.value);
|
|
5026
|
-
const label = probe.ok ?
|
|
5059
|
+
const label = probe.ok ? color2.green("ok") : probe.rejected ? color2.red("REJECTED") : color2.yellow("unverified");
|
|
5027
5060
|
process.stdout.write(` ${entry.name.padEnd(12)} ${label} ${probe.detail}
|
|
5028
5061
|
`);
|
|
5029
5062
|
if (probe.rejected) bad += 1;
|
|
@@ -5031,11 +5064,11 @@ async function keys(options = {}) {
|
|
|
5031
5064
|
return bad ? 1 : 0;
|
|
5032
5065
|
}
|
|
5033
5066
|
Ie("caddis keys");
|
|
5034
|
-
process.stdout.write(` keys file: ${
|
|
5067
|
+
process.stdout.write(` keys file: ${path20}
|
|
5035
5068
|
|
|
5036
5069
|
`);
|
|
5037
5070
|
for (const entry of current) {
|
|
5038
|
-
const state = entry.source ? `${
|
|
5071
|
+
const state = entry.source ? `${color2.green("present")} (${entry.source}) ${color2.dim(maskKey(entry.value))}` : color2.dim("missing");
|
|
5039
5072
|
process.stdout.write(` ${entry.name.padEnd(12)} ${state}
|
|
5040
5073
|
`);
|
|
5041
5074
|
}
|
|
@@ -5044,7 +5077,7 @@ async function keys(options = {}) {
|
|
|
5044
5077
|
for (const entry of current) {
|
|
5045
5078
|
if (entry.source === "env") {
|
|
5046
5079
|
process.stdout.write(
|
|
5047
|
-
|
|
5080
|
+
color2.dim(` ${entry.name}: set via $${entry.preset.keyEnv}; the environment wins, skipping
|
|
5048
5081
|
`)
|
|
5049
5082
|
);
|
|
5050
5083
|
continue;
|
|
@@ -5073,15 +5106,15 @@ async function keys(options = {}) {
|
|
|
5073
5106
|
if (!value) continue;
|
|
5074
5107
|
const probe = await probeKey(entry.preset.baseUrl, entry.preset.model, value);
|
|
5075
5108
|
if (probe.rejected) {
|
|
5076
|
-
process.stdout.write(
|
|
5109
|
+
process.stdout.write(color2.red(` ${entry.name}: ${probe.detail} \u2014 not saved
|
|
5077
5110
|
`));
|
|
5078
5111
|
continue;
|
|
5079
5112
|
}
|
|
5080
5113
|
if (!probe.ok) {
|
|
5081
|
-
process.stdout.write(
|
|
5114
|
+
process.stdout.write(color2.yellow(` ${entry.name}: ${probe.detail} \u2014 saving anyway
|
|
5082
5115
|
`));
|
|
5083
5116
|
} else {
|
|
5084
|
-
process.stdout.write(
|
|
5117
|
+
process.stdout.write(color2.green(` ${entry.name}: validated
|
|
5085
5118
|
`));
|
|
5086
5119
|
}
|
|
5087
5120
|
collected[entry.preset.keyEnv] = value;
|
|
@@ -5091,22 +5124,22 @@ async function keys(options = {}) {
|
|
|
5091
5124
|
return 0;
|
|
5092
5125
|
}
|
|
5093
5126
|
if (options.dryRun) {
|
|
5094
|
-
Se(`dry run \u2014 would write ${Object.keys(collected).join(", ")} to ${
|
|
5127
|
+
Se(`dry run \u2014 would write ${Object.keys(collected).join(", ")} to ${path20}`);
|
|
5095
5128
|
return 0;
|
|
5096
5129
|
}
|
|
5097
5130
|
let existing = "";
|
|
5098
5131
|
try {
|
|
5099
|
-
existing = readFileSync4(
|
|
5132
|
+
existing = readFileSync4(path20, "utf8");
|
|
5100
5133
|
} catch {
|
|
5101
5134
|
existing = "";
|
|
5102
5135
|
}
|
|
5103
|
-
mkdirSync2(dirname(
|
|
5104
|
-
writeFileSync(
|
|
5105
|
-
const permWarning = restrictPermissions(
|
|
5136
|
+
mkdirSync2(dirname(path20), { recursive: true });
|
|
5137
|
+
writeFileSync(path20, upsertKeyLines(existing, collected), { encoding: "utf8" });
|
|
5138
|
+
const permWarning = restrictPermissions(path20);
|
|
5106
5139
|
process.stdout.write(`
|
|
5107
|
-
wrote ${Object.keys(collected).join(", ")} to ${
|
|
5140
|
+
wrote ${Object.keys(collected).join(", ")} to ${path20}
|
|
5108
5141
|
`);
|
|
5109
|
-
if (permWarning) process.stdout.write(
|
|
5142
|
+
if (permWarning) process.stdout.write(color2.yellow(` could not set 0600 on the keys file: ${permWarning}
|
|
5110
5143
|
`));
|
|
5111
5144
|
Se("lanes ready \u2014 try `claude-glm --print-config`");
|
|
5112
5145
|
return 0;
|
|
@@ -5127,7 +5160,7 @@ __export(update_exports, {
|
|
|
5127
5160
|
});
|
|
5128
5161
|
async function update(options) {
|
|
5129
5162
|
const report = await gather(options.adapters);
|
|
5130
|
-
heading(`caddis update \u2192 pool ${
|
|
5163
|
+
heading(`caddis update \u2192 pool ${color2.bold(report.poolVersion)}`);
|
|
5131
5164
|
for (const entry of report.agents) {
|
|
5132
5165
|
if (!entry.detection.present) {
|
|
5133
5166
|
item("skip", `${entry.adapter.name} \u2014 not installed`);
|
|
@@ -5144,14 +5177,14 @@ async function update(options) {
|
|
|
5144
5177
|
const ahead = report.agents.filter((e2) => e2.drift === "ahead");
|
|
5145
5178
|
line("");
|
|
5146
5179
|
if (supportedPresent.length === 0) {
|
|
5147
|
-
line(` ${
|
|
5180
|
+
line(` ${color2.yellow("No supported agent found.")} caddis v0.1 drives Claude Code and agy.`);
|
|
5148
5181
|
} else if (ahead.length > 0) {
|
|
5149
5182
|
line(
|
|
5150
|
-
` ${
|
|
5183
|
+
` ${color2.green("Nothing to do.")} ${ahead.length} agent(s) are NEWER than this package's bundled pool (${report.poolVersion}) \u2014 driving them would be a DOWNGRADE, so they were left alone.`
|
|
5151
5184
|
);
|
|
5152
|
-
line(` Update the CLI itself (${
|
|
5185
|
+
line(` Update the CLI itself (${color2.cyan("npm i -g @caddis/cli")}), or ${color2.cyan("--force")} to roll back deliberately.`);
|
|
5153
5186
|
} else {
|
|
5154
|
-
line(` ${
|
|
5187
|
+
line(` ${color2.green("Everything is already current.")} Use --force to re-drive anyway.`);
|
|
5155
5188
|
}
|
|
5156
5189
|
return 0;
|
|
5157
5190
|
}
|
|
@@ -5284,7 +5317,7 @@ function renderEnvironment(report, problems) {
|
|
|
5284
5317
|
}
|
|
5285
5318
|
item("info", `platform ${process2.platform} ${process2.arch} (${os4.release()})`);
|
|
5286
5319
|
if (report.cliUpdate) {
|
|
5287
|
-
item("warn", `@caddis/cli ${report.cliVersion} ${
|
|
5320
|
+
item("warn", `@caddis/cli ${report.cliVersion} ${color2.dim("\u2192")} ${color2.bold(report.cliUpdate.latest)} available`);
|
|
5288
5321
|
detail('so EVERY "current" below may be wrong \u2014 it only means "matches the pool this install carries"');
|
|
5289
5322
|
problems.push({
|
|
5290
5323
|
kind: "problem",
|
|
@@ -5300,7 +5333,7 @@ function renderEnvironment(report, problems) {
|
|
|
5300
5333
|
detail(`no readable bundles/ in ${packageInfo().root}`);
|
|
5301
5334
|
problems.push({ kind: "problem", text: "this package has no shipped bundles", fix: "reinstall: npm i -g @caddis/cli" });
|
|
5302
5335
|
} else {
|
|
5303
|
-
item("ok", `shipped pool ${
|
|
5336
|
+
item("ok", `shipped pool ${color2.bold(report.poolVersion)}`);
|
|
5304
5337
|
for (const [name, version] of Object.entries(manifest.bundles)) {
|
|
5305
5338
|
detail(`bundle ${name} @ ${version}`);
|
|
5306
5339
|
}
|
|
@@ -5315,7 +5348,7 @@ function renderAgents(report, problems) {
|
|
|
5315
5348
|
const entry = report.agents[i];
|
|
5316
5349
|
const { adapter, detection, status: status5 } = entry;
|
|
5317
5350
|
if (i > 0) line("");
|
|
5318
|
-
item(markFor(entry.drift), `${
|
|
5351
|
+
item(markFor(entry.drift), `${color2.bold(adapter.name)} \u2014 ${describe(entry, report.poolVersion)}`);
|
|
5319
5352
|
if (detection.path) detail(detection.path);
|
|
5320
5353
|
if (detection.agentVersion) detail(`agent version ${detection.agentVersion}`);
|
|
5321
5354
|
if (status5.source) detail(`read from ${status5.source}`);
|
|
@@ -5325,12 +5358,12 @@ function renderAgents(report, problems) {
|
|
|
5325
5358
|
const problem = problemFor(entry, report.poolVersion);
|
|
5326
5359
|
if (problem) {
|
|
5327
5360
|
problems.push(problem);
|
|
5328
|
-
if (problem.fix) hint(
|
|
5361
|
+
if (problem.fix) hint(color2.bold(problem.fix));
|
|
5329
5362
|
}
|
|
5330
5363
|
const extrasProblem = extrasProblemFor(entry, report.extrasVersion);
|
|
5331
5364
|
if (extrasProblem) {
|
|
5332
5365
|
problems.push(extrasProblem);
|
|
5333
|
-
if (extrasProblem.fix) hint(
|
|
5366
|
+
if (extrasProblem.fix) hint(color2.bold(extrasProblem.fix));
|
|
5334
5367
|
}
|
|
5335
5368
|
}
|
|
5336
5369
|
}
|
|
@@ -5352,11 +5385,11 @@ function renderSessions(older, problems) {
|
|
|
5352
5385
|
const started = Number.isNaN(date.getTime()) ? "unknown" : `${date.toISOString().replace("T", " ").slice(0, 16)} UTC`;
|
|
5353
5386
|
item(
|
|
5354
5387
|
"warn",
|
|
5355
|
-
`${session.harness} session ${
|
|
5388
|
+
`${session.harness} session ${color2.bold(session.sessionId)} (pid ${session.pid}, started ${started}) runs caddis ${color2.yellow(session.caddisVersion ?? "?")}, installed ${color2.bold(session.installed)}`
|
|
5356
5389
|
);
|
|
5357
5390
|
}
|
|
5358
5391
|
const fix = "restart these sessions (or run /reload-plugins in each) to load the installed caddis";
|
|
5359
|
-
hint(
|
|
5392
|
+
hint(color2.bold(fix));
|
|
5360
5393
|
problems.push({
|
|
5361
5394
|
kind: "problem",
|
|
5362
5395
|
text: `${older.length} live session${older.length === 1 ? "" : "s"} still run${older.length === 1 ? "s" : ""} an older caddis`,
|
|
@@ -5428,19 +5461,19 @@ function renderSummary(report, findings) {
|
|
|
5428
5461
|
const notes = findings.filter((f) => f.kind === "note");
|
|
5429
5462
|
if (problems.length === 0) {
|
|
5430
5463
|
line(`
|
|
5431
|
-
${
|
|
5464
|
+
${color2.mint("Everything caddis manages is current.")}`);
|
|
5432
5465
|
} else {
|
|
5433
5466
|
line(`
|
|
5434
|
-
${
|
|
5467
|
+
${color2.amber(color2.bold(`${problems.length} thing${problems.length === 1 ? "" : "s"} to fix:`))}`);
|
|
5435
5468
|
for (const problem of problems) {
|
|
5436
|
-
line(` ${
|
|
5437
|
-
if (problem.fix) line(` ${
|
|
5469
|
+
line(` ${color2.dim("\u2022")} ${problem.text}`);
|
|
5470
|
+
if (problem.fix) line(` ${color2.sky(problem.fix)}`);
|
|
5438
5471
|
}
|
|
5439
5472
|
}
|
|
5440
5473
|
if (notes.length > 0) {
|
|
5441
5474
|
line(`
|
|
5442
|
-
${
|
|
5443
|
-
for (const note of notes) line(` ${
|
|
5475
|
+
${color2.dim("For your information:")}`);
|
|
5476
|
+
for (const note of notes) line(` ${color2.dim(`\u2022 ${note.text}`)}`);
|
|
5444
5477
|
}
|
|
5445
5478
|
line("");
|
|
5446
5479
|
}
|
|
@@ -5462,19 +5495,19 @@ function markFor(drift) {
|
|
|
5462
5495
|
function describe(entry, poolVersion) {
|
|
5463
5496
|
switch (entry.drift) {
|
|
5464
5497
|
case "current":
|
|
5465
|
-
return `caddis ${entry.status.version} ${
|
|
5498
|
+
return `caddis ${entry.status.version} ${color2.dim("(current)")}`;
|
|
5466
5499
|
case "ahead":
|
|
5467
|
-
return `caddis ${
|
|
5500
|
+
return `caddis ${color2.green(entry.status.version ?? "?")} ${color2.dim("\u2014 newer than this CLI bundles")} ${color2.dim(`(${poolVersion}); left alone, update @caddis/cli to catch up`)}`;
|
|
5468
5501
|
case "stale":
|
|
5469
|
-
return `caddis ${
|
|
5502
|
+
return `caddis ${color2.yellow(entry.status.version ?? "?")} ${color2.dim("\u2192")} ${color2.bold(poolVersion)} available`;
|
|
5470
5503
|
case "unknown":
|
|
5471
5504
|
return "caddis installed, version not reported";
|
|
5472
5505
|
case "missing":
|
|
5473
|
-
return
|
|
5506
|
+
return color2.yellow("caddis not installed");
|
|
5474
5507
|
case "unsupported":
|
|
5475
|
-
return
|
|
5508
|
+
return color2.dim("detected \u2014 not yet supported (v0.2)");
|
|
5476
5509
|
case "absent":
|
|
5477
|
-
return
|
|
5510
|
+
return color2.dim("not installed on this machine");
|
|
5478
5511
|
}
|
|
5479
5512
|
}
|
|
5480
5513
|
function describeExtras(entry, extrasVersion) {
|
|
@@ -5635,7 +5668,7 @@ async function status4(options) {
|
|
|
5635
5668
|
// "stays quiet when everything is current" test — correctly: a qualifier phrased like a warning
|
|
5636
5669
|
// fires on every healthy run, and a warning that always fires is the one nobody reads. doctor
|
|
5637
5670
|
// carries the strong version, because doctor is where the network check actually happens.
|
|
5638
|
-
` ${
|
|
5671
|
+
` ${color2.bold("caddis")} cli ${color2.bold(report.cliVersion)} \xB7 pool ${color2.bold(report.poolVersion)} ${color2.dim('(what "current" is measured against)')}` + (report.extrasVersion ? ` \xB7 extras ${color2.bold(report.extrasVersion)}` : "")
|
|
5639
5672
|
);
|
|
5640
5673
|
line("");
|
|
5641
5674
|
line(renderTable(["AGENT", "DETECTED", "CADDIS", "EXTRAS", "STATE"], report.agents.map(toRow)));
|
|
@@ -5645,12 +5678,12 @@ async function status4(options) {
|
|
|
5645
5678
|
);
|
|
5646
5679
|
const ahead = report.agents.filter((entry) => entry.drift === "ahead");
|
|
5647
5680
|
if (stale.length > 0) {
|
|
5648
|
-
line(` ${
|
|
5681
|
+
line(` ${color2.yellow(`${stale.length} agent(s) behind`)} \u2014 run ${color2.cyan("caddis update")} (or ${color2.cyan("caddis doctor")} for detail)`);
|
|
5649
5682
|
line("");
|
|
5650
5683
|
}
|
|
5651
5684
|
if (ahead.length > 0) {
|
|
5652
5685
|
line(
|
|
5653
|
-
` ${ahead.length} agent(s) are NEWER than this CLI's bundled pool (${report.poolVersion}) \u2014 left alone. Update the CLI itself (${
|
|
5686
|
+
` ${ahead.length} agent(s) are NEWER than this CLI's bundled pool (${report.poolVersion}) \u2014 left alone. Update the CLI itself (${color2.cyan("npm i -g @caddis/cli")}) to catch up.`
|
|
5654
5687
|
);
|
|
5655
5688
|
line("");
|
|
5656
5689
|
}
|
|
@@ -5660,30 +5693,30 @@ function toRow(entry) {
|
|
|
5660
5693
|
const managed = entry.drift !== "unsupported" && entry.drift !== "absent";
|
|
5661
5694
|
return [
|
|
5662
5695
|
entry.adapter.name,
|
|
5663
|
-
entry.detection.present ?
|
|
5664
|
-
managed && entry.status.version ||
|
|
5696
|
+
entry.detection.present ? color2.green("yes") : color2.dim("no"),
|
|
5697
|
+
managed && entry.status.version || color2.dim("\u2014"),
|
|
5665
5698
|
// Extras gets its own column rather than being folded into CADDIS: it
|
|
5666
5699
|
// versions independently, so showing one number for both would be a lie.
|
|
5667
|
-
managed && entry.status.extras?.version ||
|
|
5700
|
+
managed && entry.status.extras?.version || color2.dim("\u2014"),
|
|
5668
5701
|
stateLabel(entry.drift, entry.status.disabled ?? false)
|
|
5669
5702
|
];
|
|
5670
5703
|
}
|
|
5671
5704
|
function stateLabel(drift, disabled) {
|
|
5672
5705
|
switch (drift) {
|
|
5673
5706
|
case "current":
|
|
5674
|
-
return disabled ?
|
|
5707
|
+
return disabled ? color2.yellow("current, disabled") : color2.green("current");
|
|
5675
5708
|
case "stale":
|
|
5676
|
-
return
|
|
5709
|
+
return color2.yellow("update available");
|
|
5677
5710
|
case "ahead":
|
|
5678
|
-
return
|
|
5711
|
+
return color2.green("newer than CLI pool");
|
|
5679
5712
|
case "unknown":
|
|
5680
|
-
return
|
|
5713
|
+
return color2.yellow("version unknown");
|
|
5681
5714
|
case "missing":
|
|
5682
|
-
return
|
|
5715
|
+
return color2.yellow("not installed");
|
|
5683
5716
|
case "unsupported":
|
|
5684
|
-
return
|
|
5717
|
+
return color2.dim("v0.2");
|
|
5685
5718
|
case "absent":
|
|
5686
|
-
return
|
|
5719
|
+
return color2.dim("\u2014");
|
|
5687
5720
|
}
|
|
5688
5721
|
}
|
|
5689
5722
|
function toJson2(report) {
|
|
@@ -5707,208 +5740,2829 @@ var init_status = __esm({
|
|
|
5707
5740
|
}
|
|
5708
5741
|
});
|
|
5709
5742
|
|
|
5710
|
-
//
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
}
|
|
5743
|
+
// src/hub/paths.ts
|
|
5744
|
+
import path7 from "path";
|
|
5745
|
+
function hubDir(home = caddisHome()) {
|
|
5746
|
+
return path7.join(home, ".caddis", "hub");
|
|
5747
|
+
}
|
|
5748
|
+
var configPath, registryPath, keyPath, lockPath;
|
|
5749
|
+
var init_paths = __esm({
|
|
5750
|
+
"src/hub/paths.ts"() {
|
|
5751
|
+
"use strict";
|
|
5752
|
+
init_sessions();
|
|
5753
|
+
configPath = (home) => path7.join(hubDir(home), "config.json");
|
|
5754
|
+
registryPath = (home) => path7.join(hubDir(home), "repos.json");
|
|
5755
|
+
keyPath = (home) => path7.join(hubDir(home), "key");
|
|
5756
|
+
lockPath = (home) => path7.join(hubDir(home), "hub.lock");
|
|
5757
|
+
}
|
|
5758
|
+
});
|
|
5726
5759
|
|
|
5727
|
-
// src/
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5760
|
+
// src/hub/autostart.ts
|
|
5761
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
5762
|
+
import path8 from "path";
|
|
5763
|
+
function buildVbs(node, entry) {
|
|
5764
|
+
const q2 = (s) => `""${s.replace(/"/g, '""""')}""`;
|
|
5765
|
+
return `CreateObject("WScript.Shell").Run "${q2(node)} ${q2(entry)} hub serve", 0, False\r
|
|
5766
|
+
`;
|
|
5767
|
+
}
|
|
5768
|
+
function schtasksCreateArgs(vbs) {
|
|
5769
|
+
return ["/Create", "/SC", "ONLOGON", "/RL", "LIMITED", "/TN", TASK_NAME, "/TR", `wscript.exe "${vbs}"`, "/F"];
|
|
5770
|
+
}
|
|
5771
|
+
function buildPlist(node, entry) {
|
|
5772
|
+
const args = [node, entry, "hub", "serve"].map((a) => ` <string>${xml(a)}</string>`).join("\n");
|
|
5773
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
5774
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
5775
|
+
<plist version="1.0">
|
|
5776
|
+
<dict>
|
|
5777
|
+
<key>Label</key>
|
|
5778
|
+
<string>${MAC_LABEL}</string>
|
|
5779
|
+
<key>ProgramArguments</key>
|
|
5780
|
+
<array>
|
|
5781
|
+
${args}
|
|
5782
|
+
</array>
|
|
5783
|
+
<key>RunAtLoad</key>
|
|
5784
|
+
<true/>
|
|
5785
|
+
</dict>
|
|
5786
|
+
</plist>
|
|
5787
|
+
`;
|
|
5788
|
+
}
|
|
5789
|
+
function desktopArg(a) {
|
|
5790
|
+
const escaped = a.replace(/([`$"\\])/g, "\\$1").replace(/%/g, "%%");
|
|
5791
|
+
return `"${escaped.replace(/\\/g, "\\\\")}"`;
|
|
5792
|
+
}
|
|
5793
|
+
function buildDesktop(node, entry) {
|
|
5794
|
+
const exec = [node, entry, "hub", "serve"].map(desktopArg).join(" ");
|
|
5795
|
+
return `[Desktop Entry]
|
|
5796
|
+
Type=Application
|
|
5797
|
+
Name=caddis hub
|
|
5798
|
+
Exec=${exec}
|
|
5799
|
+
X-GNOME-Autostart-enabled=true
|
|
5800
|
+
NoDisplay=true
|
|
5801
|
+
`;
|
|
5802
|
+
}
|
|
5803
|
+
function writeText(file, text) {
|
|
5804
|
+
mkdirSync3(path8.dirname(file), { recursive: true });
|
|
5805
|
+
writeFileSync2(file, text, "utf8");
|
|
5806
|
+
}
|
|
5807
|
+
async function enableAutostart(deps = {}) {
|
|
5808
|
+
const home = deps.home ?? caddisHome();
|
|
5809
|
+
const node = deps.node ?? process.execPath;
|
|
5810
|
+
const rawEntry = deps.entry ?? process.argv[1];
|
|
5811
|
+
if (!rawEntry) return { ok: false, message: "cannot find the caddis CLI entry script" };
|
|
5812
|
+
const entry = path8.resolve(rawEntry);
|
|
5813
|
+
const runCmd = deps.runCmd ?? defaultRun;
|
|
5814
|
+
const kind = kindOf(deps.platform ?? process.platform);
|
|
5815
|
+
if (kind === "win") {
|
|
5816
|
+
const vbs = vbsPath(home);
|
|
5817
|
+
writeText(vbs, buildVbs(node, entry));
|
|
5818
|
+
const r2 = await runCmd("schtasks", schtasksCreateArgs(vbs));
|
|
5819
|
+
return r2.ok ? { ok: true, message: `caddis hub autostart enabled (logon task ${TASK_NAME})` } : { ok: false, message: `schtasks /Create failed (exit ${r2.code})` };
|
|
5820
|
+
}
|
|
5821
|
+
if (kind === "mac") {
|
|
5822
|
+
const file2 = plistPath(home);
|
|
5823
|
+
writeText(file2, buildPlist(node, entry));
|
|
5824
|
+
const r2 = await runCmd("launchctl", ["load", "-w", file2]);
|
|
5825
|
+
return r2.ok ? { ok: true, message: `caddis hub autostart enabled (${file2})` } : { ok: false, message: `launchctl load failed (exit ${r2.code})` };
|
|
5826
|
+
}
|
|
5827
|
+
const file = desktopPath(home);
|
|
5828
|
+
writeText(file, buildDesktop(node, entry));
|
|
5829
|
+
return { ok: true, message: `caddis hub autostart enabled (${file})` };
|
|
5830
|
+
}
|
|
5831
|
+
async function disableAutostart(deps = {}) {
|
|
5832
|
+
const home = deps.home ?? caddisHome();
|
|
5833
|
+
const runCmd = deps.runCmd ?? defaultRun;
|
|
5834
|
+
const kind = kindOf(deps.platform ?? process.platform);
|
|
5835
|
+
if (kind === "win") {
|
|
5836
|
+
const r2 = await runCmd("schtasks", schtasksDeleteArgs());
|
|
5837
|
+
rmSync3(vbsPath(home), { force: true });
|
|
5838
|
+
return r2.ok ? { ok: true, message: "caddis hub autostart disabled" } : { ok: true, message: "caddis hub autostart was not enabled (no logon task to delete)" };
|
|
5839
|
+
}
|
|
5840
|
+
if (kind === "mac") {
|
|
5841
|
+
const file = plistPath(home);
|
|
5842
|
+
if (existsSync6(file)) await runCmd("launchctl", ["unload", "-w", file]);
|
|
5843
|
+
rmSync3(file, { force: true });
|
|
5844
|
+
return { ok: true, message: "caddis hub autostart disabled" };
|
|
5845
|
+
}
|
|
5846
|
+
rmSync3(desktopPath(home), { force: true });
|
|
5847
|
+
return { ok: true, message: "caddis hub autostart disabled" };
|
|
5848
|
+
}
|
|
5849
|
+
async function autostartStatus(deps = {}) {
|
|
5850
|
+
const home = deps.home ?? caddisHome();
|
|
5851
|
+
const runCmd = deps.runCmd ?? defaultRun;
|
|
5852
|
+
const kind = kindOf(deps.platform ?? process.platform);
|
|
5853
|
+
const enabled = kind === "win" ? (await runCmd("schtasks", schtasksQueryArgs())).ok : existsSync6(kind === "mac" ? plistPath(home) : desktopPath(home));
|
|
5854
|
+
return { ok: enabled, message: enabled ? "caddis hub autostart is enabled" : "caddis hub autostart is not enabled" };
|
|
5855
|
+
}
|
|
5856
|
+
var TASK_NAME, MAC_LABEL, CMD_TIMEOUT_MS, vbsPath, schtasksDeleteArgs, schtasksQueryArgs, xml, plistPath, desktopPath, defaultRun, kindOf;
|
|
5857
|
+
var init_autostart = __esm({
|
|
5858
|
+
"src/hub/autostart.ts"() {
|
|
5859
|
+
"use strict";
|
|
5860
|
+
init_exec();
|
|
5861
|
+
init_sessions();
|
|
5862
|
+
init_paths();
|
|
5863
|
+
TASK_NAME = "caddis-hub";
|
|
5864
|
+
MAC_LABEL = "dev.caddis.hub";
|
|
5865
|
+
CMD_TIMEOUT_MS = 3e4;
|
|
5866
|
+
vbsPath = (home) => path8.join(hubDir(home), "autostart.vbs");
|
|
5867
|
+
schtasksDeleteArgs = () => ["/Delete", "/TN", TASK_NAME, "/F"];
|
|
5868
|
+
schtasksQueryArgs = () => ["/Query", "/TN", TASK_NAME];
|
|
5869
|
+
xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5870
|
+
plistPath = (home) => path8.join(home, "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
|
|
5871
|
+
desktopPath = (home) => path8.join(home, ".config", "autostart", `${TASK_NAME}.desktop`);
|
|
5872
|
+
defaultRun = async (cmd, args) => {
|
|
5873
|
+
const r2 = await run(cmd, args, { timeout: CMD_TIMEOUT_MS });
|
|
5874
|
+
return { ok: r2.ok, code: r2.code };
|
|
5875
|
+
};
|
|
5876
|
+
kindOf = (p2) => p2 === "win32" ? "win" : p2 === "darwin" ? "mac" : "linux";
|
|
5877
|
+
}
|
|
5878
|
+
});
|
|
5879
|
+
|
|
5880
|
+
// src/hub/config.ts
|
|
5881
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5882
|
+
import path9 from "path";
|
|
5883
|
+
import { randomBytes } from "crypto";
|
|
5884
|
+
function defaultConfig() {
|
|
5885
|
+
return { schema: 1, port: DEFAULT_PORT, accounts: {}, pinned: [], hidden: [] };
|
|
5886
|
+
}
|
|
5887
|
+
function defaultRegistry() {
|
|
5888
|
+
return { schema: 1, repos: [] };
|
|
5889
|
+
}
|
|
5890
|
+
function sleepSync(ms) {
|
|
5891
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5892
|
+
}
|
|
5893
|
+
function renameWithRetry(from, to) {
|
|
5894
|
+
const retryable = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
5895
|
+
const attempts = isWin() ? 6 : 1;
|
|
5896
|
+
for (let i = 0; ; i++) {
|
|
5897
|
+
try {
|
|
5898
|
+
renameSync2(from, to);
|
|
5899
|
+
return;
|
|
5900
|
+
} catch (e2) {
|
|
5901
|
+
const code = e2.code ?? "";
|
|
5902
|
+
if (i + 1 >= attempts || !retryable.has(code)) throw e2;
|
|
5903
|
+
sleepSync(15 * 2 ** i);
|
|
5745
5904
|
}
|
|
5746
|
-
return entry;
|
|
5747
5905
|
}
|
|
5748
|
-
return null;
|
|
5749
5906
|
}
|
|
5750
|
-
function
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5907
|
+
function writeJsonAtomic(file, data) {
|
|
5908
|
+
mkdirSync4(path9.dirname(file), { recursive: true, mode: 448 });
|
|
5909
|
+
const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
5910
|
+
try {
|
|
5911
|
+
writeFileSync3(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
5912
|
+
renameWithRetry(tmp, file);
|
|
5913
|
+
} catch (e2) {
|
|
5914
|
+
try {
|
|
5915
|
+
unlinkSync(tmp);
|
|
5916
|
+
} catch {
|
|
5760
5917
|
}
|
|
5918
|
+
throw e2;
|
|
5761
5919
|
}
|
|
5762
|
-
if (current) yield current;
|
|
5763
5920
|
}
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5921
|
+
function readJson(file, normalize, fallback) {
|
|
5922
|
+
let text;
|
|
5923
|
+
try {
|
|
5924
|
+
text = readFileSync6(file, "utf8");
|
|
5925
|
+
} catch (e2) {
|
|
5926
|
+
if (e2.code === "ENOENT") {
|
|
5927
|
+
const good = lastGood.get(file);
|
|
5928
|
+
return { value: good ?? fallback() };
|
|
5929
|
+
}
|
|
5930
|
+
return failure(file, fallback, `cannot read ${path9.basename(file)}: ${e2.message}`);
|
|
5931
|
+
}
|
|
5932
|
+
try {
|
|
5933
|
+
const value = normalize(JSON.parse(text));
|
|
5934
|
+
lastGood.set(file, value);
|
|
5935
|
+
return { value };
|
|
5936
|
+
} catch (e2) {
|
|
5937
|
+
return failure(file, fallback, `corrupt ${path9.basename(file)}: ${e2.message}`);
|
|
5768
5938
|
}
|
|
5769
|
-
const version = await run(BIN, ["--version"], { timeout: 2e4 });
|
|
5770
|
-
return {
|
|
5771
|
-
present: true,
|
|
5772
|
-
path: path7,
|
|
5773
|
-
agentVersion: version.ok ? version.stdout.trim().split(/\s+/)[0] || void 0 : void 0,
|
|
5774
|
-
note: version.ok ? void 0 : "binary found but `claude --version` failed"
|
|
5775
|
-
};
|
|
5776
5939
|
}
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
if (!
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5940
|
+
function failure(file, fallback, error) {
|
|
5941
|
+
return { value: lastGood.get(file) ?? fallback(), error };
|
|
5942
|
+
}
|
|
5943
|
+
function normalizeConfig(raw) {
|
|
5944
|
+
if (!isObj(raw)) throw new Error("not a JSON object");
|
|
5945
|
+
const d3 = defaultConfig();
|
|
5946
|
+
const port = raw.port;
|
|
5947
|
+
if (typeof port === "number" && Number.isFinite(port)) d3.port = port;
|
|
5948
|
+
if (isObj(raw.accounts)) {
|
|
5949
|
+
for (const [k3, v2] of Object.entries(raw.accounts)) if (typeof v2 === "string") d3.accounts[k3] = v2;
|
|
5950
|
+
}
|
|
5951
|
+
d3.pinned = strings(raw.pinned);
|
|
5952
|
+
d3.hidden = strings(raw.hidden);
|
|
5953
|
+
return d3;
|
|
5954
|
+
}
|
|
5955
|
+
function normalizeRegistry(raw) {
|
|
5956
|
+
if (!isObj(raw)) throw new Error("not a JSON object");
|
|
5957
|
+
const repos = [];
|
|
5958
|
+
if (Array.isArray(raw.repos)) {
|
|
5959
|
+
for (const r2 of raw.repos) {
|
|
5960
|
+
if (isObj(r2) && typeof r2.path === "string" && typeof r2.added_at === "string") {
|
|
5961
|
+
repos.push({ path: r2.path, added_at: r2.added_at });
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5787
5964
|
}
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5965
|
+
return { schema: 1, repos };
|
|
5966
|
+
}
|
|
5967
|
+
function readConfig(home) {
|
|
5968
|
+
return readJson(configPath(home), normalizeConfig, defaultConfig);
|
|
5969
|
+
}
|
|
5970
|
+
function readRegistry(home) {
|
|
5971
|
+
return readJson(registryPath(home), normalizeRegistry, defaultRegistry);
|
|
5972
|
+
}
|
|
5973
|
+
function normalizeRepoPath(p2) {
|
|
5974
|
+
const r2 = path9.resolve(p2);
|
|
5975
|
+
const root = path9.parse(r2).root;
|
|
5976
|
+
let out = r2;
|
|
5977
|
+
while (out.length > root.length && /[\/]$/.test(out)) out = out.slice(0, -1);
|
|
5978
|
+
return out;
|
|
5979
|
+
}
|
|
5980
|
+
function addRepos(paths, home) {
|
|
5981
|
+
const cur = readRegistry(home);
|
|
5982
|
+
if (cur.error) return cur;
|
|
5983
|
+
const seen = new Set(cur.value.repos.map((r2) => repoKey(r2.path)));
|
|
5984
|
+
const repos = [...cur.value.repos];
|
|
5985
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5986
|
+
let changed = false;
|
|
5987
|
+
for (const p2 of paths) {
|
|
5988
|
+
const n = normalizeRepoPath(p2);
|
|
5989
|
+
const k3 = repoKey(n);
|
|
5990
|
+
if (seen.has(k3)) continue;
|
|
5991
|
+
seen.add(k3);
|
|
5992
|
+
repos.push({ path: n, added_at: now });
|
|
5993
|
+
changed = true;
|
|
5994
|
+
}
|
|
5995
|
+
const value = { schema: 1, repos };
|
|
5996
|
+
if (changed) {
|
|
5997
|
+
writeJsonAtomic(registryPath(home), value);
|
|
5998
|
+
lastGood.set(registryPath(home), value);
|
|
5999
|
+
}
|
|
6000
|
+
return { value };
|
|
6001
|
+
}
|
|
6002
|
+
var DEFAULT_PORT, isWin, lastGood, isObj, strings, repoKey;
|
|
6003
|
+
var init_config = __esm({
|
|
6004
|
+
"src/hub/config.ts"() {
|
|
6005
|
+
"use strict";
|
|
6006
|
+
init_paths();
|
|
6007
|
+
DEFAULT_PORT = 47821;
|
|
6008
|
+
isWin = () => process.platform === "win32";
|
|
6009
|
+
lastGood = /* @__PURE__ */ new Map();
|
|
6010
|
+
isObj = (v2) => typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
|
|
6011
|
+
strings = (v2) => Array.isArray(v2) ? v2.filter((x2) => typeof x2 === "string") : [];
|
|
6012
|
+
repoKey = (p2) => isWin() ? p2.toLowerCase() : p2;
|
|
5791
6013
|
}
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
6014
|
+
});
|
|
6015
|
+
|
|
6016
|
+
// src/hub/keyfile.ts
|
|
6017
|
+
import { chmodSync as chmodSync2, linkSync, mkdirSync as mkdirSync5, readFileSync as readFileSync7, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
6018
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
6019
|
+
import path10 from "path";
|
|
6020
|
+
function windowsAccount(env = process.env) {
|
|
6021
|
+
const user = env.USERNAME ?? "";
|
|
6022
|
+
return env.USERDOMAIN ? `${env.USERDOMAIN}\\${user}` : user;
|
|
5799
6023
|
}
|
|
5800
|
-
function
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
6024
|
+
function icaclsArgs(file, account) {
|
|
6025
|
+
return [file, "/inheritance:r", "/grant:r", `${account}:F`];
|
|
6026
|
+
}
|
|
6027
|
+
function removeQuiet(file) {
|
|
6028
|
+
try {
|
|
6029
|
+
unlinkSync2(file);
|
|
6030
|
+
} catch {
|
|
5807
6031
|
}
|
|
5808
|
-
return [
|
|
5809
|
-
{ cmd: BIN, args: ["plugin", "marketplace", "update", MARKETPLACE] },
|
|
5810
|
-
{ cmd: BIN, args: ["plugin", "update", PLUGIN] }
|
|
5811
|
-
];
|
|
5812
6032
|
}
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
6033
|
+
function readKey(file, platform) {
|
|
6034
|
+
let text;
|
|
6035
|
+
try {
|
|
6036
|
+
text = readFileSync7(file, "utf8").trim();
|
|
6037
|
+
} catch (e2) {
|
|
6038
|
+
if (e2.code === "ENOENT") return null;
|
|
6039
|
+
throw e2;
|
|
5817
6040
|
}
|
|
5818
|
-
|
|
5819
|
-
if (
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
6041
|
+
if (!KEY_RE.test(text)) throw new Error(`the hub key file is corrupt: delete ${file} and start again`);
|
|
6042
|
+
if (platform !== "win32") chmodSync2(file, 384);
|
|
6043
|
+
return text;
|
|
6044
|
+
}
|
|
6045
|
+
async function ownerOnly(file, platform, deps) {
|
|
6046
|
+
if (platform !== "win32") {
|
|
6047
|
+
chmodSync2(file, 384);
|
|
6048
|
+
return;
|
|
5826
6049
|
}
|
|
5827
|
-
const
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
});
|
|
5849
|
-
if (!ok && !isMarketplaceUpdate) {
|
|
5850
|
-
return { ok: false, skipped: false, steps: results, message: `\`${command}\` failed` };
|
|
6050
|
+
const account = deps.account ?? windowsAccount();
|
|
6051
|
+
if (!account) throw new Error("icacls: cannot find the current user name (USERNAME is not set)");
|
|
6052
|
+
const r2 = await (deps.runCmd ?? run)("icacls", icaclsArgs(file, account), { timeout: ICACLS_TIMEOUT_MS });
|
|
6053
|
+
if (!r2.ok) throw new Error(`icacls could not set the owner-only ACL on the hub key file (exit ${r2.code})`);
|
|
6054
|
+
}
|
|
6055
|
+
async function ensureKey(deps = {}) {
|
|
6056
|
+
const platform = deps.platform ?? process.platform;
|
|
6057
|
+
const file = keyPath(deps.home);
|
|
6058
|
+
const existing = readKey(file, platform);
|
|
6059
|
+
if (existing) return existing;
|
|
6060
|
+
mkdirSync5(path10.dirname(file), { recursive: true, mode: 448 });
|
|
6061
|
+
const key = randomBytes2(32).toString("hex");
|
|
6062
|
+
const tmp = `${file}.${process.pid}.${randomBytes2(4).toString("hex")}.tmp`;
|
|
6063
|
+
try {
|
|
6064
|
+
writeFileSync4(tmp, "", { mode: 384, flag: "wx" });
|
|
6065
|
+
await (deps.setAcl ?? ((f) => ownerOnly(f, platform, deps)))(tmp);
|
|
6066
|
+
writeFileSync4(tmp, key, { flag: "r+" });
|
|
6067
|
+
try {
|
|
6068
|
+
linkSync(tmp, file);
|
|
6069
|
+
} catch (e2) {
|
|
6070
|
+
if (e2.code !== "EEXIST") throw e2;
|
|
5851
6071
|
}
|
|
6072
|
+
} finally {
|
|
6073
|
+
removeQuiet(tmp);
|
|
5852
6074
|
}
|
|
5853
|
-
|
|
6075
|
+
const back = readKey(file, platform);
|
|
6076
|
+
if (!back) throw new Error("the hub key file disappeared after create");
|
|
6077
|
+
return back;
|
|
5854
6078
|
}
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
status
|
|
5866
|
-
};
|
|
5867
|
-
|
|
5868
|
-
// src/agents/agy.ts
|
|
5869
|
-
init_exec();
|
|
5870
|
-
init_which();
|
|
5871
|
-
import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, mkdtempSync, mkdirSync, cpSync, rmSync, renameSync } from "fs";
|
|
5872
|
-
import os from "os";
|
|
5873
|
-
import path2 from "path";
|
|
6079
|
+
var KEY_RE, ICACLS_TIMEOUT_MS;
|
|
6080
|
+
var init_keyfile = __esm({
|
|
6081
|
+
"src/hub/keyfile.ts"() {
|
|
6082
|
+
"use strict";
|
|
6083
|
+
init_paths();
|
|
6084
|
+
init_exec();
|
|
6085
|
+
KEY_RE = /^[0-9a-f]{64}$/;
|
|
6086
|
+
ICACLS_TIMEOUT_MS = 15e3;
|
|
6087
|
+
}
|
|
6088
|
+
});
|
|
5874
6089
|
|
|
5875
|
-
// src/
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
const
|
|
5881
|
-
|
|
6090
|
+
// src/hub/lock.ts
|
|
6091
|
+
import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeSync } from "fs";
|
|
6092
|
+
import path11 from "path";
|
|
6093
|
+
function readLock(home) {
|
|
6094
|
+
try {
|
|
6095
|
+
const raw = JSON.parse(readFileSync8(lockPath(home), "utf8"));
|
|
6096
|
+
const pid = raw.pid;
|
|
6097
|
+
const port = raw.port;
|
|
6098
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return null;
|
|
6099
|
+
if (typeof port !== "number" || !Number.isInteger(port)) return null;
|
|
6100
|
+
const started = typeof raw.started_at === "string" ? raw.started_at : "";
|
|
6101
|
+
const info = { pid, started_at: started, port };
|
|
6102
|
+
if (typeof raw.instance === "string" && /^[0-9a-f]{32}$/.test(raw.instance)) info.instance = raw.instance;
|
|
6103
|
+
return info;
|
|
6104
|
+
} catch {
|
|
6105
|
+
return null;
|
|
5882
6106
|
}
|
|
5883
|
-
return pids;
|
|
5884
6107
|
}
|
|
5885
|
-
function
|
|
5886
|
-
return
|
|
6108
|
+
function holderAlive(h2, home, alive) {
|
|
6109
|
+
return h2.pid === process.pid ? held.has(lockPath(home)) : alive(h2.pid);
|
|
5887
6110
|
}
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
6111
|
+
function removeFile(file) {
|
|
6112
|
+
try {
|
|
6113
|
+
unlinkSync3(file);
|
|
6114
|
+
} catch (e2) {
|
|
6115
|
+
if (e2.code !== "ENOENT") throw e2;
|
|
5892
6116
|
}
|
|
5893
|
-
const result = await run("pgrep", ["-x", "agy"], { timeout: 1e4 });
|
|
5894
|
-
return result.ok ? parsePgrep(result.stdout) : [];
|
|
5895
6117
|
}
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
6118
|
+
function acquireLock(info, home, alive = pidAlive) {
|
|
6119
|
+
const file = lockPath(home);
|
|
6120
|
+
const live = (h2) => h2 !== null && holderAlive(h2, home, alive);
|
|
6121
|
+
mkdirSync6(path11.dirname(file), { recursive: true, mode: 448 });
|
|
6122
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
6123
|
+
let fd;
|
|
6124
|
+
try {
|
|
6125
|
+
fd = openSync(file, "wx", 384);
|
|
6126
|
+
} catch (e2) {
|
|
6127
|
+
if (e2.code !== "EEXIST") throw e2;
|
|
6128
|
+
const holder2 = readLock(home);
|
|
6129
|
+
if (live(holder2)) return { ok: false, holder: holder2 };
|
|
6130
|
+
const again = readLock(home);
|
|
6131
|
+
if (live(again)) return { ok: false, holder: again };
|
|
6132
|
+
removeFile(file);
|
|
6133
|
+
continue;
|
|
6134
|
+
}
|
|
6135
|
+
try {
|
|
6136
|
+
writeSync(fd, JSON.stringify(info) + "\n");
|
|
6137
|
+
} finally {
|
|
6138
|
+
closeSync(fd);
|
|
6139
|
+
}
|
|
6140
|
+
if (info.pid === process.pid) held.add(file);
|
|
6141
|
+
return { ok: true };
|
|
6142
|
+
}
|
|
6143
|
+
const holder = readLock(home);
|
|
6144
|
+
if (holder) return { ok: false, holder };
|
|
6145
|
+
throw new Error("could not take the hub lock");
|
|
5908
6146
|
}
|
|
5909
|
-
function
|
|
5910
|
-
|
|
5911
|
-
|
|
6147
|
+
function writeLock(info, home) {
|
|
6148
|
+
writeJsonAtomic(lockPath(home), info);
|
|
6149
|
+
}
|
|
6150
|
+
function releaseLock(pid, home) {
|
|
6151
|
+
const cur = readLock(home);
|
|
6152
|
+
if (!cur || cur.pid !== pid) return;
|
|
6153
|
+
if (pid === process.pid) held.delete(lockPath(home));
|
|
6154
|
+
try {
|
|
6155
|
+
removeFile(lockPath(home));
|
|
6156
|
+
} catch {
|
|
6157
|
+
}
|
|
6158
|
+
}
|
|
6159
|
+
function removeLockIf(expected, home) {
|
|
6160
|
+
const cur = readLock(home);
|
|
6161
|
+
if (cur && cur.pid === expected.pid && cur.instance === expected.instance && cur.started_at === expected.started_at) {
|
|
6162
|
+
removeLock(home);
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
function removeLock(home) {
|
|
6166
|
+
held.delete(lockPath(home));
|
|
6167
|
+
try {
|
|
6168
|
+
removeFile(lockPath(home));
|
|
6169
|
+
} catch {
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
var held;
|
|
6173
|
+
var init_lock = __esm({
|
|
6174
|
+
"src/hub/lock.ts"() {
|
|
6175
|
+
"use strict";
|
|
6176
|
+
init_config();
|
|
6177
|
+
init_paths();
|
|
6178
|
+
init_sessions();
|
|
6179
|
+
held = /* @__PURE__ */ new Set();
|
|
6180
|
+
}
|
|
6181
|
+
});
|
|
6182
|
+
|
|
6183
|
+
// src/hub/restart.ts
|
|
6184
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
6185
|
+
import path12 from "path";
|
|
6186
|
+
function readInstalledVersion2(packageRoot) {
|
|
6187
|
+
try {
|
|
6188
|
+
const parsed = JSON.parse(readFileSync9(path12.join(packageRoot, "package.json"), "utf8"));
|
|
6189
|
+
return typeof parsed.version === "string" && parsed.version ? parsed.version : null;
|
|
6190
|
+
} catch {
|
|
6191
|
+
return null;
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
function createRestarter(deps) {
|
|
6195
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((r2) => setTimeout(r2, ms)));
|
|
6196
|
+
const now = deps.now ?? Date.now;
|
|
6197
|
+
const waitMs = deps.waitMs ?? RESTART_WAIT_MS;
|
|
6198
|
+
const failed = /* @__PURE__ */ new Set();
|
|
6199
|
+
let busy = false;
|
|
6200
|
+
let cancel = null;
|
|
6201
|
+
async function handOver(version) {
|
|
6202
|
+
deps.log(`caddis hub: version ${deps.currentVersion} -> ${version}; starting the new server`);
|
|
6203
|
+
let child = null;
|
|
6204
|
+
let error = "";
|
|
6205
|
+
try {
|
|
6206
|
+
await deps.closeServer();
|
|
6207
|
+
child = await deps.spawnNew();
|
|
6208
|
+
let exited = false;
|
|
6209
|
+
void child.exited.then(() => {
|
|
6210
|
+
exited = true;
|
|
6211
|
+
});
|
|
6212
|
+
const deadline = now() + waitMs;
|
|
6213
|
+
while (now() < deadline) {
|
|
6214
|
+
await Promise.resolve();
|
|
6215
|
+
if (exited) {
|
|
6216
|
+
error = "the new server exited before it answered";
|
|
6217
|
+
break;
|
|
6218
|
+
}
|
|
6219
|
+
if (await deps.probe(child) === version) {
|
|
6220
|
+
deps.exit(0);
|
|
6221
|
+
return;
|
|
6222
|
+
}
|
|
6223
|
+
await sleep2(POLL_MS);
|
|
6224
|
+
}
|
|
6225
|
+
if (!error) error = `the new server did not answer with version ${version} in ${Math.round(waitMs / 1e3)} s`;
|
|
6226
|
+
} catch (e2) {
|
|
6227
|
+
error = e2 instanceof Error ? e2.message : String(e2);
|
|
6228
|
+
}
|
|
6229
|
+
failed.add(version);
|
|
6230
|
+
deps.log(`caddis hub: restart to ${version} failed (${error}); keeping version ${deps.currentVersion}`);
|
|
6231
|
+
if (child && deps.killChild) {
|
|
6232
|
+
try {
|
|
6233
|
+
await deps.killChild(child);
|
|
6234
|
+
} catch {
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
try {
|
|
6238
|
+
await deps.restartOwn();
|
|
6239
|
+
} catch (e2) {
|
|
6240
|
+
deps.log(`caddis hub: could not restart the old server: ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
6241
|
+
deps.exit(1);
|
|
6242
|
+
}
|
|
6243
|
+
}
|
|
6244
|
+
async function tick() {
|
|
6245
|
+
if (busy) return;
|
|
6246
|
+
busy = true;
|
|
6247
|
+
try {
|
|
6248
|
+
const v2 = deps.readVersion();
|
|
6249
|
+
if (v2 === null || v2 === deps.currentVersion || failed.has(v2)) return;
|
|
6250
|
+
await handOver(v2);
|
|
6251
|
+
} catch (e2) {
|
|
6252
|
+
try {
|
|
6253
|
+
deps.log(`caddis hub: restart check failed: ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
6254
|
+
} catch {
|
|
6255
|
+
}
|
|
6256
|
+
} finally {
|
|
6257
|
+
busy = false;
|
|
6258
|
+
}
|
|
6259
|
+
}
|
|
6260
|
+
return {
|
|
6261
|
+
start: () => {
|
|
6262
|
+
cancel ??= deps.every(() => void tick(), RESTART_CHECK_MS);
|
|
6263
|
+
},
|
|
6264
|
+
stop: () => {
|
|
6265
|
+
cancel?.();
|
|
6266
|
+
cancel = null;
|
|
6267
|
+
},
|
|
6268
|
+
tick
|
|
6269
|
+
};
|
|
6270
|
+
}
|
|
6271
|
+
var RESTART_CHECK_MS, RESTART_WAIT_MS, POLL_MS;
|
|
6272
|
+
var init_restart = __esm({
|
|
6273
|
+
"src/hub/restart.ts"() {
|
|
6274
|
+
"use strict";
|
|
6275
|
+
RESTART_CHECK_MS = 6e4;
|
|
6276
|
+
RESTART_WAIT_MS = 2e4;
|
|
6277
|
+
POLL_MS = 250;
|
|
6278
|
+
}
|
|
6279
|
+
});
|
|
6280
|
+
|
|
6281
|
+
// src/hub/auth.ts
|
|
6282
|
+
import { createHash, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
6283
|
+
function randomToken() {
|
|
6284
|
+
return randomBytes3(32).toString("base64url");
|
|
6285
|
+
}
|
|
6286
|
+
function safeEqual(a, b3) {
|
|
6287
|
+
const ha = createHash("sha256").update(a, "utf8").digest();
|
|
6288
|
+
const hb = createHash("sha256").update(b3, "utf8").digest();
|
|
6289
|
+
return timingSafeEqual(ha, hb) && a.length === b3.length;
|
|
6290
|
+
}
|
|
6291
|
+
function hostAllowed(host, port) {
|
|
6292
|
+
if (!host) return false;
|
|
6293
|
+
const h2 = host.toLowerCase();
|
|
6294
|
+
return h2 === `127.0.0.1:${port}` || h2 === `localhost:${port}`;
|
|
6295
|
+
}
|
|
6296
|
+
function originAllowed(origin, port) {
|
|
6297
|
+
if (!origin) return false;
|
|
6298
|
+
return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;
|
|
6299
|
+
}
|
|
6300
|
+
function parseCookies(header) {
|
|
6301
|
+
const out = {};
|
|
6302
|
+
if (!header) return out;
|
|
6303
|
+
for (const part of header.split(";")) {
|
|
6304
|
+
const i = part.indexOf("=");
|
|
6305
|
+
if (i < 1) continue;
|
|
6306
|
+
const name = part.slice(0, i).trim();
|
|
6307
|
+
if (name && !(name in out)) out[name] = part.slice(i + 1).trim();
|
|
6308
|
+
}
|
|
6309
|
+
return out;
|
|
6310
|
+
}
|
|
6311
|
+
var CODE_TTL_MS, MAX_CODES, MAX_SESSIONS, SESSION_IDLE_MS, SESSION_MAX_MS, AuthStore;
|
|
6312
|
+
var init_auth = __esm({
|
|
6313
|
+
"src/hub/auth.ts"() {
|
|
6314
|
+
"use strict";
|
|
6315
|
+
CODE_TTL_MS = 6e4;
|
|
6316
|
+
MAX_CODES = 20;
|
|
6317
|
+
MAX_SESSIONS = 20;
|
|
6318
|
+
SESSION_IDLE_MS = 12 * 60 * 60 * 1e3;
|
|
6319
|
+
SESSION_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
6320
|
+
AuthStore = class {
|
|
6321
|
+
constructor(now = Date.now) {
|
|
6322
|
+
this.now = now;
|
|
6323
|
+
}
|
|
6324
|
+
now;
|
|
6325
|
+
codes = [];
|
|
6326
|
+
sessions = [];
|
|
6327
|
+
/** Mint a single-use code. Expired codes are dropped first; then the oldest goes past the cap. */
|
|
6328
|
+
mintCode() {
|
|
6329
|
+
const t = this.now();
|
|
6330
|
+
this.codes = this.codes.filter((c) => t - c.mintedAt < CODE_TTL_MS);
|
|
6331
|
+
const value = randomToken();
|
|
6332
|
+
this.codes.push({ value, mintedAt: t });
|
|
6333
|
+
while (this.codes.length > MAX_CODES) this.codes.shift();
|
|
6334
|
+
return value;
|
|
6335
|
+
}
|
|
6336
|
+
/** Expiry time of a code minted now, as UTC ISO 8601. */
|
|
6337
|
+
codeExpiresAt() {
|
|
6338
|
+
return new Date(this.now() + CODE_TTL_MS).toISOString();
|
|
6339
|
+
}
|
|
6340
|
+
/**
|
|
6341
|
+
* Swap a code for a new session id. The code is removed before the age check,
|
|
6342
|
+
* so a code is consumed even when this call fails. Returns null on failure.
|
|
6343
|
+
*/
|
|
6344
|
+
redeemCode(code) {
|
|
6345
|
+
if (!code) return null;
|
|
6346
|
+
let found;
|
|
6347
|
+
for (const c of this.codes) if (safeEqual(c.value, code)) found = c;
|
|
6348
|
+
if (!found) return null;
|
|
6349
|
+
this.codes = this.codes.filter((c) => c !== found);
|
|
6350
|
+
const age = this.now() - found.mintedAt;
|
|
6351
|
+
if (age < 0 || age >= CODE_TTL_MS) return null;
|
|
6352
|
+
const t = this.now();
|
|
6353
|
+
const s = { id: randomToken(), csrf: randomToken(), tab: randomToken(), createdAt: t, lastSeenAt: t };
|
|
6354
|
+
this.sessions.push(s);
|
|
6355
|
+
while (this.sessions.length > MAX_SESSIONS) this.sessions.shift();
|
|
6356
|
+
return s.id;
|
|
6357
|
+
}
|
|
6358
|
+
/** Remove a code without making a session (a refused /login still uses up its code). */
|
|
6359
|
+
discardCode(code) {
|
|
6360
|
+
if (!code) return;
|
|
6361
|
+
this.codes = this.codes.filter((c) => !safeEqual(c.value, code));
|
|
6362
|
+
}
|
|
6363
|
+
/**
|
|
6364
|
+
* Find a live session and mark it used. A session past the idle TTL or the absolute TTL
|
|
6365
|
+
* is removed and the call returns null.
|
|
6366
|
+
*/
|
|
6367
|
+
getSession(id) {
|
|
6368
|
+
return this.findSession(id, null);
|
|
6369
|
+
}
|
|
6370
|
+
/**
|
|
6371
|
+
* As getSession, but the session's tab token must match too (constant time). A wrong or missing token
|
|
6372
|
+
* returns null and does not mark the session used.
|
|
6373
|
+
*/
|
|
6374
|
+
getTabSession(id, tab) {
|
|
6375
|
+
return this.findSession(id, tab ?? "");
|
|
6376
|
+
}
|
|
6377
|
+
findSession(id, tab) {
|
|
6378
|
+
if (!id) return null;
|
|
6379
|
+
let found = null;
|
|
6380
|
+
for (const s of this.sessions) if (safeEqual(s.id, id)) found = s;
|
|
6381
|
+
if (!found) return null;
|
|
6382
|
+
if (tab !== null && !safeEqual(found.tab, tab)) return null;
|
|
6383
|
+
const t = this.now();
|
|
6384
|
+
if (t - found.lastSeenAt >= SESSION_IDLE_MS || t - found.createdAt >= SESSION_MAX_MS) {
|
|
6385
|
+
this.sessions = this.sessions.filter((x2) => x2 !== found);
|
|
6386
|
+
return null;
|
|
6387
|
+
}
|
|
6388
|
+
found.lastSeenAt = t;
|
|
6389
|
+
return found;
|
|
6390
|
+
}
|
|
6391
|
+
deleteSession(id) {
|
|
6392
|
+
const s = this.getSession(id);
|
|
6393
|
+
if (s) this.sessions = this.sessions.filter((x2) => x2 !== s);
|
|
6394
|
+
}
|
|
6395
|
+
codeCount() {
|
|
6396
|
+
return this.codes.length;
|
|
6397
|
+
}
|
|
6398
|
+
sessionCount() {
|
|
6399
|
+
return this.sessions.length;
|
|
6400
|
+
}
|
|
6401
|
+
};
|
|
6402
|
+
}
|
|
6403
|
+
});
|
|
6404
|
+
|
|
6405
|
+
// src/hub/server.ts
|
|
6406
|
+
import http from "http";
|
|
6407
|
+
import { existsSync as existsSync7 } from "fs";
|
|
6408
|
+
import { readFile, stat } from "fs/promises";
|
|
6409
|
+
import path13 from "path";
|
|
6410
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6411
|
+
function defaultUiDir() {
|
|
6412
|
+
const here = path13.dirname(fileURLToPath2(import.meta.url));
|
|
6413
|
+
const beside = path13.join(here, "ui");
|
|
6414
|
+
return existsSync7(beside) ? beside : path13.resolve(here, "..", "..", "dist", "ui");
|
|
6415
|
+
}
|
|
6416
|
+
function resolveStatic(root, urlPath) {
|
|
6417
|
+
let decoded;
|
|
6418
|
+
try {
|
|
6419
|
+
decoded = decodeURIComponent(urlPath);
|
|
6420
|
+
} catch {
|
|
6421
|
+
return null;
|
|
6422
|
+
}
|
|
6423
|
+
if (decoded.includes(String.fromCharCode(0)) || decoded.includes(String.fromCharCode(92))) return null;
|
|
6424
|
+
if (decoded.split("/").includes("..")) return null;
|
|
6425
|
+
const base = path13.resolve(root);
|
|
6426
|
+
const file = path13.resolve(base, "." + decoded);
|
|
6427
|
+
return file.startsWith(base + path13.sep) ? file : null;
|
|
6428
|
+
}
|
|
6429
|
+
function send(res, status5, body, extra = {}) {
|
|
6430
|
+
const text = JSON.stringify(body);
|
|
6431
|
+
res.writeHead(status5, {
|
|
6432
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
6433
|
+
"Content-Length": Buffer.byteLength(text),
|
|
6434
|
+
...extra
|
|
6435
|
+
});
|
|
6436
|
+
res.end(text);
|
|
6437
|
+
}
|
|
6438
|
+
function readBody(req) {
|
|
6439
|
+
return new Promise((resolve, reject) => {
|
|
6440
|
+
const declared = Number(req.headers["content-length"] ?? 0);
|
|
6441
|
+
if (declared > MAX_BODY_BYTES) {
|
|
6442
|
+
reject(new BodyTooLarge());
|
|
6443
|
+
return;
|
|
6444
|
+
}
|
|
6445
|
+
const chunks = [];
|
|
6446
|
+
let size = 0;
|
|
6447
|
+
let done = false;
|
|
6448
|
+
req.on("data", (c) => {
|
|
6449
|
+
if (done) return;
|
|
6450
|
+
size += c.length;
|
|
6451
|
+
if (size > MAX_BODY_BYTES) {
|
|
6452
|
+
done = true;
|
|
6453
|
+
reject(new BodyTooLarge());
|
|
6454
|
+
return;
|
|
6455
|
+
}
|
|
6456
|
+
chunks.push(c);
|
|
6457
|
+
});
|
|
6458
|
+
req.on("end", () => {
|
|
6459
|
+
if (!done) resolve(Buffer.concat(chunks));
|
|
6460
|
+
});
|
|
6461
|
+
req.on("error", (e2) => {
|
|
6462
|
+
if (!done) reject(e2);
|
|
6463
|
+
});
|
|
6464
|
+
});
|
|
6465
|
+
}
|
|
6466
|
+
function tooLarge(req, res) {
|
|
6467
|
+
res.on("finish", () => req.socket.destroy());
|
|
6468
|
+
fail(res, 413, "body_too_large");
|
|
6469
|
+
}
|
|
6470
|
+
async function serveStatic(res, uiDir, urlPath) {
|
|
6471
|
+
const rel = urlPath === "/" ? "/index.html" : urlPath;
|
|
6472
|
+
const file = resolveStatic(uiDir, rel);
|
|
6473
|
+
if (!file) return fail(res, 404, "not_found");
|
|
6474
|
+
let body;
|
|
6475
|
+
try {
|
|
6476
|
+
if (!(await stat(file)).isFile()) return fail(res, 404, "not_found");
|
|
6477
|
+
body = await readFile(file);
|
|
6478
|
+
} catch {
|
|
6479
|
+
if (urlPath === "/") {
|
|
6480
|
+
res.writeHead(503, {
|
|
6481
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6482
|
+
"Content-Length": Buffer.byteLength(NOT_BUILT_HTML),
|
|
6483
|
+
"Cache-Control": "no-store"
|
|
6484
|
+
});
|
|
6485
|
+
res.end(NOT_BUILT_HTML);
|
|
6486
|
+
return;
|
|
6487
|
+
}
|
|
6488
|
+
return fail(res, 404, "not_found");
|
|
6489
|
+
}
|
|
6490
|
+
res.writeHead(200, {
|
|
6491
|
+
"Content-Type": MIME[path13.extname(file).toLowerCase()] ?? "application/octet-stream",
|
|
6492
|
+
"Content-Length": body.length,
|
|
6493
|
+
"Cache-Control": urlPath === "/" ? "no-store" : "public, max-age=31536000, immutable"
|
|
6494
|
+
});
|
|
6495
|
+
res.end(body);
|
|
6496
|
+
}
|
|
6497
|
+
function keyOk(req, key) {
|
|
6498
|
+
const v2 = req.headers[KEY_HEADER];
|
|
6499
|
+
return typeof v2 === "string" && safeEqual(v2, key);
|
|
6500
|
+
}
|
|
6501
|
+
async function startServer(opts) {
|
|
6502
|
+
const host = opts.host ?? "127.0.0.1";
|
|
6503
|
+
if (host !== "127.0.0.1") throw new Error("the hub server binds 127.0.0.1 only");
|
|
6504
|
+
if (typeof opts.key !== "string" || Buffer.byteLength(opts.key, "utf8") < MIN_KEY_BYTES) {
|
|
6505
|
+
throw new Error(`the hub server key must be at least ${MIN_KEY_BYTES} bytes`);
|
|
6506
|
+
}
|
|
6507
|
+
const auth = new AuthStore(opts.now ?? Date.now);
|
|
6508
|
+
const { key, versions } = opts;
|
|
6509
|
+
const uiDir = opts.uiDir ?? defaultUiDir();
|
|
6510
|
+
let port = opts.port;
|
|
6511
|
+
const sessionOf = (req) => auth.getSession(parseCookies(req.headers.cookie)[SESSION_COOKIE]);
|
|
6512
|
+
const tabSessionOf = (req) => {
|
|
6513
|
+
const tab = req.headers[TAB_HEADER];
|
|
6514
|
+
return auth.getTabSession(parseCookies(req.headers.cookie)[SESSION_COOKIE], typeof tab === "string" ? tab : "");
|
|
6515
|
+
};
|
|
6516
|
+
async function handle(req, res) {
|
|
6517
|
+
res.setHeader("Content-Security-Policy", CSP);
|
|
6518
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
6519
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
6520
|
+
let url;
|
|
6521
|
+
try {
|
|
6522
|
+
url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
6523
|
+
} catch {
|
|
6524
|
+
fail(res, 400, "bad_url");
|
|
6525
|
+
return;
|
|
6526
|
+
}
|
|
6527
|
+
const path20 = url.pathname;
|
|
6528
|
+
const method = req.method ?? "GET";
|
|
6529
|
+
if (path20.startsWith("/api/") || path20 === "/login") res.setHeader("Cache-Control", "no-store");
|
|
6530
|
+
if (!hostAllowed(req.headers.host, port)) {
|
|
6531
|
+
if (path20 === "/login") auth.discardCode(url.searchParams.get("code") ?? "");
|
|
6532
|
+
fail(res, 403, "bad_host");
|
|
6533
|
+
return;
|
|
6534
|
+
}
|
|
6535
|
+
if (path20 === "/api/hub/whoami" && method === "GET") {
|
|
6536
|
+
if (!opts.instance) return fail(res, 404, "not_found");
|
|
6537
|
+
return send(res, 200, { instance: opts.instance });
|
|
6538
|
+
}
|
|
6539
|
+
if (path20 === "/api/health" && method === "GET") {
|
|
6540
|
+
if (!keyOk(req, key)) return fail(res, 401, "unauthorized");
|
|
6541
|
+
return send(res, 200, { schema: 1, ok: true, versions });
|
|
6542
|
+
}
|
|
6543
|
+
if (path20 === "/api/hub/code" && method === "POST") {
|
|
6544
|
+
if (!keyOk(req, key)) return fail(res, 401, "unauthorized");
|
|
6545
|
+
try {
|
|
6546
|
+
await readBody(req);
|
|
6547
|
+
} catch (e2) {
|
|
6548
|
+
if (e2 instanceof BodyTooLarge) return tooLarge(req, res);
|
|
6549
|
+
throw e2;
|
|
6550
|
+
}
|
|
6551
|
+
const code = auth.mintCode();
|
|
6552
|
+
return send(res, 200, { schema: 1, code, expires_at: auth.codeExpiresAt() });
|
|
6553
|
+
}
|
|
6554
|
+
if (path20 === "/api/hub/stop" && method === "POST") {
|
|
6555
|
+
if (!keyOk(req, key)) return fail(res, 401, "unauthorized");
|
|
6556
|
+
try {
|
|
6557
|
+
await readBody(req);
|
|
6558
|
+
} catch (e2) {
|
|
6559
|
+
if (e2 instanceof BodyTooLarge) return tooLarge(req, res);
|
|
6560
|
+
throw e2;
|
|
6561
|
+
}
|
|
6562
|
+
res.on("finish", () => opts.onStop?.());
|
|
6563
|
+
return send(res, 200, { schema: 1, ok: true });
|
|
6564
|
+
}
|
|
6565
|
+
if (method !== "GET") {
|
|
6566
|
+
const origin = req.headers.origin;
|
|
6567
|
+
if (!originAllowed(typeof origin === "string" ? origin : void 0, port)) return fail(res, 403, "bad_origin");
|
|
6568
|
+
const session = sessionOf(req);
|
|
6569
|
+
if (!session) return fail(res, 401, "unauthorized");
|
|
6570
|
+
const token = req.headers[CSRF_HEADER];
|
|
6571
|
+
if (typeof token !== "string" || !safeEqual(token, session.csrf)) return fail(res, 403, "bad_csrf");
|
|
6572
|
+
try {
|
|
6573
|
+
await readBody(req);
|
|
6574
|
+
} catch (e2) {
|
|
6575
|
+
if (e2 instanceof BodyTooLarge) return tooLarge(req, res);
|
|
6576
|
+
throw e2;
|
|
6577
|
+
}
|
|
6578
|
+
if (path20 === "/api/logout" && method === "POST") {
|
|
6579
|
+
auth.deleteSession(session.id);
|
|
6580
|
+
return send(res, 200, { schema: 1, ok: true }, {
|
|
6581
|
+
"Set-Cookie": `${SESSION_COOKIE}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0`
|
|
6582
|
+
});
|
|
6583
|
+
}
|
|
6584
|
+
return fail(res, 404, "not_found");
|
|
6585
|
+
}
|
|
6586
|
+
if (path20 === "/" || path20.startsWith("/assets/") || path20.startsWith("/fonts/")) {
|
|
6587
|
+
await serveStatic(res, uiDir, path20);
|
|
6588
|
+
return;
|
|
6589
|
+
}
|
|
6590
|
+
if (path20 === "/login") {
|
|
6591
|
+
const id = auth.redeemCode(url.searchParams.get("code") ?? "");
|
|
6592
|
+
const session = id ? auth.getSession(id) : null;
|
|
6593
|
+
if (!id || !session) return fail(res, 401, "bad_code");
|
|
6594
|
+
res.writeHead(303, {
|
|
6595
|
+
Location: `/#tab=${session.tab}`,
|
|
6596
|
+
"Set-Cookie": `${SESSION_COOKIE}=${id}; HttpOnly; SameSite=Strict; Path=/`,
|
|
6597
|
+
"Content-Length": 0
|
|
6598
|
+
});
|
|
6599
|
+
res.end();
|
|
6600
|
+
return;
|
|
6601
|
+
}
|
|
6602
|
+
if (path20 === "/api/session") {
|
|
6603
|
+
const session = tabSessionOf(req);
|
|
6604
|
+
if (!session) return fail(res, 401, "unauthorized");
|
|
6605
|
+
return send(res, 200, { schema: 1, csrf_token: session.csrf, versions });
|
|
6606
|
+
}
|
|
6607
|
+
if (path20 === "/api/state") {
|
|
6608
|
+
if (!tabSessionOf(req)) return fail(res, 401, "unauthorized");
|
|
6609
|
+
let state;
|
|
6610
|
+
try {
|
|
6611
|
+
state = await opts.getState();
|
|
6612
|
+
} catch {
|
|
6613
|
+
return fail(res, 500, "state_failed");
|
|
6614
|
+
}
|
|
6615
|
+
return send(res, 200, state);
|
|
6616
|
+
}
|
|
6617
|
+
return fail(res, 404, "not_found");
|
|
6618
|
+
}
|
|
6619
|
+
const server = http.createServer((req, res) => {
|
|
6620
|
+
handle(req, res).catch(() => {
|
|
6621
|
+
if (!res.headersSent) fail(res, 500, "internal");
|
|
6622
|
+
else res.destroy();
|
|
6623
|
+
});
|
|
6624
|
+
});
|
|
6625
|
+
server.headersTimeout = 1e4;
|
|
6626
|
+
server.requestTimeout = 15e3;
|
|
6627
|
+
await new Promise((resolve, reject) => {
|
|
6628
|
+
const onError = (e2) => reject(e2);
|
|
6629
|
+
server.once("error", onError);
|
|
6630
|
+
server.listen(opts.port, host, () => {
|
|
6631
|
+
server.off("error", onError);
|
|
6632
|
+
resolve();
|
|
6633
|
+
});
|
|
6634
|
+
});
|
|
6635
|
+
const addr = server.address();
|
|
6636
|
+
port = addr.port;
|
|
6637
|
+
return {
|
|
6638
|
+
port,
|
|
6639
|
+
address: addr.address,
|
|
6640
|
+
close: () => new Promise((resolve) => {
|
|
6641
|
+
server.close(() => resolve());
|
|
6642
|
+
server.closeAllConnections();
|
|
6643
|
+
})
|
|
6644
|
+
};
|
|
6645
|
+
}
|
|
6646
|
+
var SESSION_COOKIE, KEY_HEADER, CSRF_HEADER, TAB_HEADER, MAX_BODY_BYTES, MIN_KEY_BYTES, CSP, NOT_BUILT_HTML, MIME, BodyTooLarge, fail;
|
|
6647
|
+
var init_server = __esm({
|
|
6648
|
+
"src/hub/server.ts"() {
|
|
6649
|
+
"use strict";
|
|
6650
|
+
init_auth();
|
|
6651
|
+
SESSION_COOKIE = "caddis_hub_session";
|
|
6652
|
+
KEY_HEADER = "x-caddis-hub-key";
|
|
6653
|
+
CSRF_HEADER = "x-csrf-token";
|
|
6654
|
+
TAB_HEADER = "x-caddis-hub-tab";
|
|
6655
|
+
MAX_BODY_BYTES = 16 * 1024;
|
|
6656
|
+
MIN_KEY_BYTES = 32;
|
|
6657
|
+
CSP = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'";
|
|
6658
|
+
NOT_BUILT_HTML = '<!doctype html><html lang="en"><head><meta charset="utf-8"><title>caddis hub</title></head><body>UI not built: run npm run build</body></html>';
|
|
6659
|
+
MIME = {
|
|
6660
|
+
".html": "text/html; charset=utf-8",
|
|
6661
|
+
".js": "text/javascript; charset=utf-8",
|
|
6662
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
6663
|
+
".css": "text/css; charset=utf-8",
|
|
6664
|
+
".json": "application/json; charset=utf-8",
|
|
6665
|
+
".svg": "image/svg+xml",
|
|
6666
|
+
".png": "image/png",
|
|
6667
|
+
".ico": "image/x-icon",
|
|
6668
|
+
".woff2": "font/woff2",
|
|
6669
|
+
".txt": "text/plain; charset=utf-8",
|
|
6670
|
+
".map": "application/json; charset=utf-8"
|
|
6671
|
+
};
|
|
6672
|
+
BodyTooLarge = class extends Error {
|
|
6673
|
+
};
|
|
6674
|
+
fail = (res, status5, error) => send(res, status5, { schema: 1, error });
|
|
6675
|
+
}
|
|
6676
|
+
});
|
|
6677
|
+
|
|
6678
|
+
// src/hub/bounded.ts
|
|
6679
|
+
function withTimeout(work, ms, what) {
|
|
6680
|
+
let timer;
|
|
6681
|
+
const limit = new Promise((_resolve, reject) => {
|
|
6682
|
+
timer = setTimeout(() => reject(new Error(`${what} did not finish in ${Math.round(ms / 1e3)} s`)), ms);
|
|
6683
|
+
timer.unref();
|
|
6684
|
+
});
|
|
6685
|
+
return Promise.race([work, limit]).finally(() => clearTimeout(timer));
|
|
6686
|
+
}
|
|
6687
|
+
var init_bounded = __esm({
|
|
6688
|
+
"src/hub/bounded.ts"() {
|
|
6689
|
+
"use strict";
|
|
6690
|
+
}
|
|
6691
|
+
});
|
|
6692
|
+
|
|
6693
|
+
// src/hub/spawn.ts
|
|
6694
|
+
import { spawn } from "child_process";
|
|
6695
|
+
function killTree(pid) {
|
|
6696
|
+
if (process.platform === "win32") {
|
|
6697
|
+
try {
|
|
6698
|
+
spawn("taskkill", ["/T", "/F", "/PID", String(pid)], {
|
|
6699
|
+
windowsHide: true,
|
|
6700
|
+
shell: false,
|
|
6701
|
+
stdio: "ignore"
|
|
6702
|
+
}).on("error", () => {
|
|
6703
|
+
});
|
|
6704
|
+
} catch {
|
|
6705
|
+
}
|
|
6706
|
+
return;
|
|
6707
|
+
}
|
|
6708
|
+
try {
|
|
6709
|
+
process.kill(-pid, "SIGKILL");
|
|
6710
|
+
} catch {
|
|
6711
|
+
try {
|
|
6712
|
+
process.kill(pid, "SIGKILL");
|
|
6713
|
+
} catch {
|
|
6714
|
+
}
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
function wrapWindowsShim(cmd, args) {
|
|
6718
|
+
if (process.platform !== "win32" || !/\.(cmd|bat)$/i.test(cmd)) return null;
|
|
6719
|
+
if (UNSAFE_PATH.test(cmd)) return { error: `refused: the command path contains an unsafe character: ${cmd}` };
|
|
6720
|
+
for (const a of args) {
|
|
6721
|
+
if (!SAFE_ARG.test(a)) return { error: `refused argument for a .cmd shim: ${JSON.stringify(a)}` };
|
|
6722
|
+
}
|
|
6723
|
+
return { cmd: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", `""${cmd}"${args.map((a) => ` ${a}`).join("")}"`] };
|
|
6724
|
+
}
|
|
6725
|
+
var DEFAULT_MAX_BYTES, SAFE_ARG, UNSAFE_PATH, spawnBounded;
|
|
6726
|
+
var init_spawn = __esm({
|
|
6727
|
+
"src/hub/spawn.ts"() {
|
|
6728
|
+
"use strict";
|
|
6729
|
+
DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
6730
|
+
SAFE_ARG = /^[A-Za-z0-9._:/@=-]+$/;
|
|
6731
|
+
UNSAFE_PATH = /["&|<>^%!\r\n]/;
|
|
6732
|
+
spawnBounded = (cmd, args, opts) => new Promise((resolve) => {
|
|
6733
|
+
let verbatim = false;
|
|
6734
|
+
const wrapped = wrapWindowsShim(cmd, args);
|
|
6735
|
+
if (wrapped && "error" in wrapped) {
|
|
6736
|
+
resolve({ ok: false, code: null, stdout: "", stderr: wrapped.error, timedOut: false });
|
|
6737
|
+
return;
|
|
6738
|
+
}
|
|
6739
|
+
if (wrapped) {
|
|
6740
|
+
verbatim = true;
|
|
6741
|
+
cmd = wrapped.cmd;
|
|
6742
|
+
args = wrapped.args;
|
|
6743
|
+
}
|
|
6744
|
+
const cap = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
6745
|
+
let stdout = "";
|
|
6746
|
+
let stderr = "";
|
|
6747
|
+
let timedOut = false;
|
|
6748
|
+
let settled = false;
|
|
6749
|
+
const finish2 = (code, extraErr = "") => {
|
|
6750
|
+
if (settled) return;
|
|
6751
|
+
settled = true;
|
|
6752
|
+
clearTimeout(timer);
|
|
6753
|
+
resolve({
|
|
6754
|
+
ok: code === 0 && !timedOut,
|
|
6755
|
+
code,
|
|
6756
|
+
stdout,
|
|
6757
|
+
stderr: extraErr ? `${stderr}${stderr ? "\n" : ""}${extraErr}` : stderr,
|
|
6758
|
+
timedOut
|
|
6759
|
+
});
|
|
6760
|
+
};
|
|
6761
|
+
let child;
|
|
6762
|
+
try {
|
|
6763
|
+
child = spawn(cmd, args, {
|
|
6764
|
+
cwd: opts.cwd,
|
|
6765
|
+
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
6766
|
+
windowsHide: true,
|
|
6767
|
+
shell: false,
|
|
6768
|
+
// The wrapped command line is already quoted for cmd.exe. Node must not quote it again.
|
|
6769
|
+
windowsVerbatimArguments: verbatim,
|
|
6770
|
+
detached: process.platform !== "win32",
|
|
6771
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6772
|
+
});
|
|
6773
|
+
} catch (error) {
|
|
6774
|
+
resolve({
|
|
6775
|
+
ok: false,
|
|
6776
|
+
code: null,
|
|
6777
|
+
stdout: "",
|
|
6778
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
6779
|
+
timedOut: false
|
|
6780
|
+
});
|
|
6781
|
+
return;
|
|
6782
|
+
}
|
|
6783
|
+
const timer = setTimeout(() => {
|
|
6784
|
+
timedOut = true;
|
|
6785
|
+
if (child.pid !== void 0) killTree(child.pid);
|
|
6786
|
+
setTimeout(() => finish2(null), 2e3).unref();
|
|
6787
|
+
}, opts.timeoutMs);
|
|
6788
|
+
const append = (current, chunk) => {
|
|
6789
|
+
if (current.length >= cap) return current;
|
|
6790
|
+
return (current + chunk.toString("utf8")).slice(0, cap);
|
|
6791
|
+
};
|
|
6792
|
+
child.stdout?.on("data", (c) => {
|
|
6793
|
+
stdout = append(stdout, c);
|
|
6794
|
+
});
|
|
6795
|
+
child.stderr?.on("data", (c) => {
|
|
6796
|
+
stderr = append(stderr, c);
|
|
6797
|
+
});
|
|
6798
|
+
child.on("error", (error) => finish2(null, error.message));
|
|
6799
|
+
child.on("close", (code) => finish2(code));
|
|
6800
|
+
});
|
|
6801
|
+
}
|
|
6802
|
+
});
|
|
6803
|
+
|
|
6804
|
+
// src/hub/sources/agents.ts
|
|
6805
|
+
import { existsSync as existsSync8, readFileSync as readFileSync10, statSync } from "fs";
|
|
6806
|
+
import path14 from "path";
|
|
6807
|
+
function classifyWaiting(waitingFor) {
|
|
6808
|
+
const w2 = (waitingFor ?? "").toLowerCase();
|
|
6809
|
+
if (w2 === "permission prompt" || w2 === "input needed" || w2.includes("question")) return "owner";
|
|
6810
|
+
return "peer";
|
|
6811
|
+
}
|
|
6812
|
+
function repoPathFor(cwd) {
|
|
6813
|
+
if (!cwd) return null;
|
|
6814
|
+
let dir = path14.resolve(cwd);
|
|
6815
|
+
for (; ; ) {
|
|
6816
|
+
const dotGit = path14.join(dir, ".git");
|
|
6817
|
+
if (existsSync8(dotGit)) {
|
|
6818
|
+
if (isDir(dotGit)) return normalizeRepoPath(dir);
|
|
6819
|
+
if (isFile(dotGit)) return commonRoot(dir, dotGit);
|
|
6820
|
+
return null;
|
|
6821
|
+
}
|
|
6822
|
+
const parent = path14.dirname(dir);
|
|
6823
|
+
if (parent === dir) return null;
|
|
6824
|
+
dir = parent;
|
|
6825
|
+
}
|
|
6826
|
+
}
|
|
6827
|
+
function commonRoot(dir, dotGitFile) {
|
|
6828
|
+
try {
|
|
6829
|
+
const m2 = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync10(dotGitFile, "utf8"));
|
|
6830
|
+
if (!m2) return null;
|
|
6831
|
+
const gitdir = path14.resolve(dir, m2[1] ?? "");
|
|
6832
|
+
let common;
|
|
6833
|
+
try {
|
|
6834
|
+
const rel = readFileSync10(path14.join(gitdir, "commondir"), "utf8").trim();
|
|
6835
|
+
if (!rel) return normalizeRepoPath(dir);
|
|
6836
|
+
common = path14.resolve(gitdir, rel);
|
|
6837
|
+
} catch {
|
|
6838
|
+
return normalizeRepoPath(dir);
|
|
6839
|
+
}
|
|
6840
|
+
return normalizeRepoPath(path14.dirname(common));
|
|
6841
|
+
} catch {
|
|
6842
|
+
return null;
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
6845
|
+
function createAgentsSource(opts = {}) {
|
|
6846
|
+
const now = opts.now ?? Date.now;
|
|
6847
|
+
const spawn2 = opts.spawn ?? spawnBounded;
|
|
6848
|
+
const find2 = opts.findClaude ?? (() => findBin("claude"));
|
|
6849
|
+
const waitingSince = /* @__PURE__ */ new Map();
|
|
6850
|
+
let sessions = null;
|
|
6851
|
+
let lastOkAt = null;
|
|
6852
|
+
let lastError = null;
|
|
6853
|
+
let lastRunOk = false;
|
|
6854
|
+
let running = null;
|
|
6855
|
+
const fail2 = (message3) => {
|
|
6856
|
+
lastError = message3;
|
|
6857
|
+
lastRunOk = false;
|
|
6858
|
+
};
|
|
6859
|
+
const ingest = (parsed) => {
|
|
6860
|
+
if (!Array.isArray(parsed)) return fail2("claude agents output is not a JSON array");
|
|
6861
|
+
const t = now();
|
|
6862
|
+
const byId = /* @__PURE__ */ new Map();
|
|
6863
|
+
const seenWaiting = /* @__PURE__ */ new Set();
|
|
6864
|
+
for (const raw of parsed) {
|
|
6865
|
+
if (!raw || typeof raw !== "object") continue;
|
|
6866
|
+
const e2 = raw;
|
|
6867
|
+
const sessionId = str(e2.sessionId);
|
|
6868
|
+
if (!sessionId) continue;
|
|
6869
|
+
const kind = str(e2.kind) ?? "interactive";
|
|
6870
|
+
const status5 = str(e2.status) ?? str(e2.state) ?? "unknown";
|
|
6871
|
+
if (kind === "background" && status5 === "done") continue;
|
|
6872
|
+
const prior = byId.get(sessionId);
|
|
6873
|
+
if (prior) {
|
|
6874
|
+
prior.copies += 1;
|
|
6875
|
+
continue;
|
|
6876
|
+
}
|
|
6877
|
+
const pid = typeof e2.pid === "number" ? e2.pid : null;
|
|
6878
|
+
const cwd = str(e2.cwd);
|
|
6879
|
+
const isWaiting = status5 === "waiting";
|
|
6880
|
+
const waitingFor = isWaiting ? str(e2.waitingFor) : null;
|
|
6881
|
+
const key = `${pid ?? str(e2.id) ?? kind}:${sessionId}`;
|
|
6882
|
+
let since = null;
|
|
6883
|
+
if (isWaiting) {
|
|
6884
|
+
seenWaiting.add(key);
|
|
6885
|
+
since = waitingSince.get(key) ?? t;
|
|
6886
|
+
waitingSince.set(key, since);
|
|
6887
|
+
}
|
|
6888
|
+
const confirmed = isWaiting && t - (since ?? t) >= WAIT_DEBOUNCE_MS;
|
|
6889
|
+
byId.set(sessionId, {
|
|
6890
|
+
key,
|
|
6891
|
+
pid,
|
|
6892
|
+
session_id: sessionId,
|
|
6893
|
+
name: str(e2.name),
|
|
6894
|
+
kind,
|
|
6895
|
+
cwd,
|
|
6896
|
+
repo_path: repoPathFor(cwd),
|
|
6897
|
+
status: status5,
|
|
6898
|
+
waiting_for: waitingFor,
|
|
6899
|
+
waiting_class: confirmed ? classifyWaiting(waitingFor) : null,
|
|
6900
|
+
waiting_since: since === null ? null : new Date(since).toISOString(),
|
|
6901
|
+
copies: 1
|
|
6902
|
+
});
|
|
6903
|
+
}
|
|
6904
|
+
for (const k3 of [...waitingSince.keys()]) if (!seenWaiting.has(k3)) waitingSince.delete(k3);
|
|
6905
|
+
sessions = [...byId.values()];
|
|
6906
|
+
lastOkAt = t;
|
|
6907
|
+
lastError = null;
|
|
6908
|
+
lastRunOk = true;
|
|
6909
|
+
};
|
|
6910
|
+
const run2 = async () => {
|
|
6911
|
+
const bin = await withTimeout(find2(), FIND_TIMEOUT_MS, "finding claude");
|
|
6912
|
+
if (!bin) return fail2("claude was not found on PATH");
|
|
6913
|
+
const result = await spawn2(bin, ["agents", "--json"], { timeoutMs: RUN_TIMEOUT_MS });
|
|
6914
|
+
if (result.timedOut) return fail2(`claude agents timed out after ${RUN_TIMEOUT_MS / 1e3} s`);
|
|
6915
|
+
if (!result.ok) {
|
|
6916
|
+
const detail2 = (result.stderr || result.stdout).trim().slice(0, 500);
|
|
6917
|
+
return fail2(`claude agents failed (exit ${result.code ?? "none"})${detail2 ? `: ${detail2}` : ""}`);
|
|
6918
|
+
}
|
|
6919
|
+
let parsed;
|
|
6920
|
+
try {
|
|
6921
|
+
parsed = JSON.parse(result.stdout);
|
|
6922
|
+
} catch (error) {
|
|
6923
|
+
return fail2(`claude agents output is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
6924
|
+
}
|
|
6925
|
+
ingest(parsed);
|
|
6926
|
+
};
|
|
6927
|
+
return {
|
|
6928
|
+
refresh() {
|
|
6929
|
+
if (!running) {
|
|
6930
|
+
running = run2().catch((error) => fail2(error instanceof Error ? error.message : String(error))).finally(() => {
|
|
6931
|
+
running = null;
|
|
6932
|
+
});
|
|
6933
|
+
}
|
|
6934
|
+
return running;
|
|
6935
|
+
},
|
|
6936
|
+
get() {
|
|
6937
|
+
return {
|
|
6938
|
+
ok: lastRunOk,
|
|
6939
|
+
last_ok_at: lastOkAt === null ? null : new Date(lastOkAt).toISOString(),
|
|
6940
|
+
last_error: lastError,
|
|
6941
|
+
stale: lastOkAt === null || now() - lastOkAt > STALE_AFTER_MS2,
|
|
6942
|
+
data: sessions
|
|
6943
|
+
};
|
|
6944
|
+
}
|
|
6945
|
+
};
|
|
6946
|
+
}
|
|
6947
|
+
var STALE_AFTER_MS2, WAIT_DEBOUNCE_MS, RUN_TIMEOUT_MS, FIND_TIMEOUT_MS, isFile, isDir, str;
|
|
6948
|
+
var init_agents = __esm({
|
|
6949
|
+
"src/hub/sources/agents.ts"() {
|
|
6950
|
+
"use strict";
|
|
6951
|
+
init_which();
|
|
6952
|
+
init_bounded();
|
|
6953
|
+
init_spawn();
|
|
6954
|
+
init_config();
|
|
6955
|
+
STALE_AFTER_MS2 = 3 * 6e4;
|
|
6956
|
+
WAIT_DEBOUNCE_MS = 45e3;
|
|
6957
|
+
RUN_TIMEOUT_MS = 1e4;
|
|
6958
|
+
FIND_TIMEOUT_MS = 1e4;
|
|
6959
|
+
isFile = (p2) => {
|
|
6960
|
+
try {
|
|
6961
|
+
return statSync(p2).isFile();
|
|
6962
|
+
} catch {
|
|
6963
|
+
return false;
|
|
6964
|
+
}
|
|
6965
|
+
};
|
|
6966
|
+
isDir = (p2) => {
|
|
6967
|
+
try {
|
|
6968
|
+
return statSync(p2).isDirectory();
|
|
6969
|
+
} catch {
|
|
6970
|
+
return false;
|
|
6971
|
+
}
|
|
6972
|
+
};
|
|
6973
|
+
str = (v2) => typeof v2 === "string" && v2 ? v2 : null;
|
|
6974
|
+
}
|
|
6975
|
+
});
|
|
6976
|
+
|
|
6977
|
+
// src/hub/sources/collector.ts
|
|
6978
|
+
import { existsSync as existsSync9 } from "fs";
|
|
6979
|
+
import os5 from "os";
|
|
6980
|
+
import path15 from "path";
|
|
6981
|
+
function defaultScriptPath() {
|
|
6982
|
+
const name = "caddis_hub_collect.py";
|
|
6983
|
+
const bundle = bundlePath("antigravity-plugin", { requireManifest: false });
|
|
6984
|
+
if (bundle) {
|
|
6985
|
+
const shipped = path15.join(bundle, "scripts", name);
|
|
6986
|
+
if (existsSync9(shipped)) return shipped;
|
|
6987
|
+
}
|
|
6988
|
+
return path15.join(packageInfo().root, "..", "scripts", name);
|
|
6989
|
+
}
|
|
6990
|
+
async function resolvePython() {
|
|
6991
|
+
const py = await find("python");
|
|
6992
|
+
if (py) return { cmd: py, pre: [] };
|
|
6993
|
+
if (process.platform === "win32") {
|
|
6994
|
+
const launcher = await find("py");
|
|
6995
|
+
if (launcher) return { cmd: launcher, pre: ["-3"] };
|
|
6996
|
+
return null;
|
|
6997
|
+
}
|
|
6998
|
+
const py3 = await find("python3");
|
|
6999
|
+
return py3 ? { cmd: py3, pre: [] } : null;
|
|
7000
|
+
}
|
|
7001
|
+
function createCollectorSource(opts) {
|
|
7002
|
+
const now = opts.now ?? Date.now;
|
|
7003
|
+
const spawn2 = opts.spawn ?? spawnBounded;
|
|
7004
|
+
const cache = /* @__PURE__ */ new Map();
|
|
7005
|
+
let spend = null;
|
|
7006
|
+
let guide = null;
|
|
7007
|
+
let schema = COLLECTOR_SCHEMA;
|
|
7008
|
+
let generatedAt = "";
|
|
7009
|
+
let lastOkAt = null;
|
|
7010
|
+
let lastError = null;
|
|
7011
|
+
let updateNeeded = false;
|
|
7012
|
+
let lastRunOk = false;
|
|
7013
|
+
let running = null;
|
|
7014
|
+
const fail2 = (message3) => {
|
|
7015
|
+
lastError = message3;
|
|
7016
|
+
lastRunOk = false;
|
|
7017
|
+
};
|
|
7018
|
+
const ingest = (parsed, wanted) => {
|
|
7019
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return fail2("collector output is not a JSON object");
|
|
7020
|
+
const out = parsed;
|
|
7021
|
+
const version = out.schema;
|
|
7022
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version < COLLECTOR_SCHEMA - 1) {
|
|
7023
|
+
return fail2(`collector output has an unusable schema: ${String(version)}`);
|
|
7024
|
+
}
|
|
7025
|
+
if (version > COLLECTOR_SCHEMA) {
|
|
7026
|
+
updateNeeded = true;
|
|
7027
|
+
return fail2(`collector schema ${version} is newer than this hub (${COLLECTOR_SCHEMA}): update the hub`);
|
|
7028
|
+
}
|
|
7029
|
+
if (!Array.isArray(out.repos)) return fail2("collector output has no repos list");
|
|
7030
|
+
updateNeeded = false;
|
|
7031
|
+
schema = version;
|
|
7032
|
+
generatedAt = typeof out.generated_at === "string" ? out.generated_at : "";
|
|
7033
|
+
const keep = new Set(wanted);
|
|
7034
|
+
for (const key of [...cache.keys()]) if (!keep.has(key)) cache.delete(key);
|
|
7035
|
+
for (const entry of out.repos) {
|
|
7036
|
+
if (!entry || typeof entry.path !== "string" || !keep.has(entry.path)) continue;
|
|
7037
|
+
const old = cache.get(entry.path);
|
|
7038
|
+
if (entry.error !== void 0 || entry.fingerprint === void 0) {
|
|
7039
|
+
if (!old || old.error !== void 0) cache.set(entry.path, entry);
|
|
7040
|
+
continue;
|
|
7041
|
+
}
|
|
7042
|
+
if (!old || old.error !== void 0 || old.fingerprint !== entry.fingerprint) cache.set(entry.path, entry);
|
|
7043
|
+
}
|
|
7044
|
+
const keepOrError = (next, old) => {
|
|
7045
|
+
if (!next || typeof next !== "object") return old;
|
|
7046
|
+
const slot = next;
|
|
7047
|
+
if ("error" in slot && old && !("error" in old)) return old;
|
|
7048
|
+
return slot;
|
|
7049
|
+
};
|
|
7050
|
+
spend = keepOrError(out.spend, spend);
|
|
7051
|
+
guide = keepOrError(out.guide, guide);
|
|
7052
|
+
lastOkAt = now();
|
|
7053
|
+
lastError = null;
|
|
7054
|
+
lastRunOk = true;
|
|
7055
|
+
};
|
|
7056
|
+
const run2 = async () => {
|
|
7057
|
+
const repos = opts.repos();
|
|
7058
|
+
let cmd = opts.python;
|
|
7059
|
+
let pre = [];
|
|
7060
|
+
if (!cmd) {
|
|
7061
|
+
const found = await resolvePython();
|
|
7062
|
+
if (!found) return fail2("python was not found (tried python, then py -3)");
|
|
7063
|
+
cmd = found.cmd;
|
|
7064
|
+
pre = found.pre;
|
|
7065
|
+
}
|
|
7066
|
+
const script = opts.scriptPath ?? defaultScriptPath();
|
|
7067
|
+
const args = repos.length ? [...pre, script, "--repos", ...repos, "--json"] : [...pre, script, "--repos", path15.join(os5.tmpdir(), "caddis-hub-no-repos"), "--only", "spend,guide", "--json"];
|
|
7068
|
+
const result = await spawn2(cmd, args, { timeoutMs: RUN_TIMEOUT_MS2 });
|
|
7069
|
+
if (result.timedOut) return fail2(`collector timed out after ${RUN_TIMEOUT_MS2 / 1e3} s`);
|
|
7070
|
+
if (!result.ok) {
|
|
7071
|
+
const detail2 = (result.stderr || result.stdout).trim().slice(0, 500);
|
|
7072
|
+
return fail2(`collector failed (exit ${result.code ?? "none"})${detail2 ? `: ${detail2}` : ""}`);
|
|
7073
|
+
}
|
|
7074
|
+
let parsed;
|
|
7075
|
+
try {
|
|
7076
|
+
parsed = JSON.parse(result.stdout);
|
|
7077
|
+
} catch (error) {
|
|
7078
|
+
return fail2(`collector output is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
7079
|
+
}
|
|
7080
|
+
ingest(parsed, repos);
|
|
7081
|
+
};
|
|
7082
|
+
return {
|
|
7083
|
+
refresh() {
|
|
7084
|
+
if (!running) {
|
|
7085
|
+
running = run2().catch((error) => fail2(error instanceof Error ? error.message : String(error))).finally(() => {
|
|
7086
|
+
running = null;
|
|
7087
|
+
});
|
|
7088
|
+
}
|
|
7089
|
+
return running;
|
|
7090
|
+
},
|
|
7091
|
+
get() {
|
|
7092
|
+
const hasData = lastOkAt !== null;
|
|
7093
|
+
return {
|
|
7094
|
+
ok: lastRunOk,
|
|
7095
|
+
last_ok_at: lastOkAt === null ? null : new Date(lastOkAt).toISOString(),
|
|
7096
|
+
last_error: lastError,
|
|
7097
|
+
stale: lastOkAt === null || now() - lastOkAt > STALE_AFTER_MS3,
|
|
7098
|
+
update_needed: updateNeeded,
|
|
7099
|
+
data: hasData ? { schema, generated_at: generatedAt, repos: [...cache.values()], spend: spend ?? {}, guide: guide ?? {} } : null
|
|
7100
|
+
};
|
|
7101
|
+
}
|
|
7102
|
+
};
|
|
7103
|
+
}
|
|
7104
|
+
var COLLECTOR_SCHEMA, STALE_AFTER_MS3, RUN_TIMEOUT_MS2, FIND_TIMEOUT_MS2, find;
|
|
7105
|
+
var init_collector = __esm({
|
|
7106
|
+
"src/hub/sources/collector.ts"() {
|
|
7107
|
+
"use strict";
|
|
7108
|
+
init_pkg();
|
|
7109
|
+
init_which();
|
|
7110
|
+
init_bounded();
|
|
7111
|
+
init_spawn();
|
|
7112
|
+
COLLECTOR_SCHEMA = 1;
|
|
7113
|
+
STALE_AFTER_MS3 = 3 * 6e4;
|
|
7114
|
+
RUN_TIMEOUT_MS2 = 6e4;
|
|
7115
|
+
FIND_TIMEOUT_MS2 = 1e4;
|
|
7116
|
+
find = (name) => withTimeout(findBin(name), FIND_TIMEOUT_MS2, `finding ${name}`);
|
|
7117
|
+
}
|
|
7118
|
+
});
|
|
7119
|
+
|
|
7120
|
+
// src/hub/sources/coordinator.ts
|
|
7121
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
|
|
7122
|
+
import path16 from "path";
|
|
7123
|
+
function stub(repoId, schema) {
|
|
7124
|
+
return {
|
|
7125
|
+
repo_id: repoId,
|
|
7126
|
+
schema,
|
|
7127
|
+
coordinator_session: null,
|
|
7128
|
+
heartbeat_at: null,
|
|
7129
|
+
queue: [],
|
|
7130
|
+
merging_now: null,
|
|
7131
|
+
deploy_checks: [],
|
|
7132
|
+
ledger: [],
|
|
7133
|
+
processed_requests: []
|
|
7134
|
+
};
|
|
7135
|
+
}
|
|
7136
|
+
function parse3(repoId, text) {
|
|
7137
|
+
let raw;
|
|
7138
|
+
try {
|
|
7139
|
+
raw = JSON.parse(text);
|
|
7140
|
+
} catch (e2) {
|
|
7141
|
+
return { error: `corrupt coordinator.json: ${e2.message}` };
|
|
7142
|
+
}
|
|
7143
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { error: "coordinator.json is not a JSON object" };
|
|
7144
|
+
const o2 = raw;
|
|
7145
|
+
const schema = o2.schema;
|
|
7146
|
+
if (typeof schema !== "number" || !Number.isInteger(schema) || schema < COORDINATOR_SCHEMA - 1) {
|
|
7147
|
+
return { error: `coordinator.json has an unusable schema: ${String(schema)}` };
|
|
7148
|
+
}
|
|
7149
|
+
if (schema > COORDINATOR_SCHEMA) {
|
|
7150
|
+
return {
|
|
7151
|
+
newer: stub(repoId, schema),
|
|
7152
|
+
error: `coordinator schema ${schema} is newer than this hub (${COORDINATOR_SCHEMA}): update the hub`
|
|
7153
|
+
};
|
|
7154
|
+
}
|
|
7155
|
+
return {
|
|
7156
|
+
good: {
|
|
7157
|
+
repo_id: repoId,
|
|
7158
|
+
schema,
|
|
7159
|
+
coordinator_session: o2.coordinator_session ?? null,
|
|
7160
|
+
heartbeat_at: typeof o2.heartbeat_at === "string" ? o2.heartbeat_at : null,
|
|
7161
|
+
queue: arr(o2.queue),
|
|
7162
|
+
merging_now: o2.merging_now ?? null,
|
|
7163
|
+
deploy_checks: arr(o2.deploy_checks).slice(-5),
|
|
7164
|
+
ledger: arr(o2.ledger).slice(-20),
|
|
7165
|
+
processed_requests: arr(o2.processed_requests).slice(-20)
|
|
7166
|
+
}
|
|
7167
|
+
};
|
|
7168
|
+
}
|
|
7169
|
+
function hubHome(home) {
|
|
7170
|
+
const override = process.env.CADDIS_HUB_HOME;
|
|
7171
|
+
return override ? override : hubDir(home);
|
|
7172
|
+
}
|
|
7173
|
+
function createCoordinatorSource(opts = {}) {
|
|
7174
|
+
const now = opts.now ?? Date.now;
|
|
7175
|
+
const lastGood2 = /* @__PURE__ */ new Map();
|
|
7176
|
+
const errors = /* @__PURE__ */ new Map();
|
|
7177
|
+
return {
|
|
7178
|
+
refresh() {
|
|
7179
|
+
const root = path16.join(hubHome(opts.home), "repos");
|
|
7180
|
+
let ids = [];
|
|
7181
|
+
try {
|
|
7182
|
+
ids = readdirSync3(root, { withFileTypes: true }).filter((d3) => d3.isDirectory()).map((d3) => d3.name);
|
|
7183
|
+
} catch {
|
|
7184
|
+
}
|
|
7185
|
+
const seen = new Set(ids);
|
|
7186
|
+
for (const id of [...lastGood2.keys()]) if (!seen.has(id)) lastGood2.delete(id);
|
|
7187
|
+
for (const id of [...errors.keys()]) if (!seen.has(id)) errors.delete(id);
|
|
7188
|
+
for (const id of ids) {
|
|
7189
|
+
let text;
|
|
7190
|
+
try {
|
|
7191
|
+
text = readFileSync11(path16.join(root, id, "coordinator.json"), "utf8");
|
|
7192
|
+
} catch (e2) {
|
|
7193
|
+
if (e2.code === "ENOENT") {
|
|
7194
|
+
lastGood2.delete(id);
|
|
7195
|
+
errors.delete(id);
|
|
7196
|
+
} else {
|
|
7197
|
+
if (!lastGood2.has(id)) lastGood2.set(id, stub(id, null));
|
|
7198
|
+
errors.set(id, { error: `cannot read coordinator.json: ${e2.message}`, updateNeeded: false });
|
|
7199
|
+
}
|
|
7200
|
+
continue;
|
|
7201
|
+
}
|
|
7202
|
+
const r2 = parse3(id, text);
|
|
7203
|
+
if (r2.good) {
|
|
7204
|
+
lastGood2.set(id, r2.good);
|
|
7205
|
+
errors.delete(id);
|
|
7206
|
+
continue;
|
|
7207
|
+
}
|
|
7208
|
+
if (!lastGood2.has(id)) lastGood2.set(id, r2.newer ?? stub(id, null));
|
|
7209
|
+
errors.set(id, { error: r2.error ?? "unreadable", updateNeeded: r2.newer !== void 0 });
|
|
7210
|
+
}
|
|
7211
|
+
},
|
|
7212
|
+
get() {
|
|
7213
|
+
const t = now();
|
|
7214
|
+
const out = [];
|
|
7215
|
+
for (const [id, good] of lastGood2) {
|
|
7216
|
+
const ms = good.heartbeat_at ? Date.parse(good.heartbeat_at) : NaN;
|
|
7217
|
+
const age = Number.isNaN(ms) ? null : Math.max(0, Math.floor((t - ms) / 1e3));
|
|
7218
|
+
const err = errors.get(id);
|
|
7219
|
+
out.push({
|
|
7220
|
+
...good,
|
|
7221
|
+
heartbeat_age_s: age,
|
|
7222
|
+
responding: age !== null && age < RESPONDING_WITHIN_S,
|
|
7223
|
+
error: err?.error ?? null,
|
|
7224
|
+
update_needed: err?.updateNeeded ?? false
|
|
7225
|
+
});
|
|
7226
|
+
}
|
|
7227
|
+
return out.sort((a, b3) => a.repo_id.localeCompare(b3.repo_id));
|
|
7228
|
+
}
|
|
7229
|
+
};
|
|
7230
|
+
}
|
|
7231
|
+
var COORDINATOR_SCHEMA, RESPONDING_WITHIN_S, arr;
|
|
7232
|
+
var init_coordinator = __esm({
|
|
7233
|
+
"src/hub/sources/coordinator.ts"() {
|
|
7234
|
+
"use strict";
|
|
7235
|
+
init_paths();
|
|
7236
|
+
COORDINATOR_SCHEMA = 1;
|
|
7237
|
+
RESPONDING_WITHIN_S = 180;
|
|
7238
|
+
arr = (v2) => Array.isArray(v2) ? v2 : [];
|
|
7239
|
+
}
|
|
7240
|
+
});
|
|
7241
|
+
|
|
7242
|
+
// src/hub/sources/github.ts
|
|
7243
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
7244
|
+
import path17 from "path";
|
|
7245
|
+
function parseGithubRemote(url) {
|
|
7246
|
+
const m2 = /^(?:https?:\/\/(?:[^@/]+@)?|ssh:\/\/git@|git@)github\.com[/:]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i.exec(url.trim());
|
|
7247
|
+
return m2 ? `${m2[1]}/${m2[2]}` : null;
|
|
7248
|
+
}
|
|
7249
|
+
function readOriginUrl(repoDir) {
|
|
7250
|
+
const root = repoPathFor(repoDir);
|
|
7251
|
+
if (!root) return null;
|
|
7252
|
+
let text;
|
|
7253
|
+
try {
|
|
7254
|
+
text = readFileSync12(path17.join(root, ".git", "config"), "utf8");
|
|
7255
|
+
} catch {
|
|
7256
|
+
return null;
|
|
7257
|
+
}
|
|
7258
|
+
let inOrigin = false;
|
|
7259
|
+
for (const line2 of text.split(/\r?\n/)) {
|
|
7260
|
+
const section = /^\s*\[(.+)\]\s*$/.exec(line2);
|
|
7261
|
+
if (section) {
|
|
7262
|
+
inOrigin = /^remote\s+"origin"$/.test(section[1] ?? "");
|
|
7263
|
+
continue;
|
|
7264
|
+
}
|
|
7265
|
+
const kv = inOrigin ? /^\s*url\s*=\s*(.+?)\s*$/.exec(line2) : null;
|
|
7266
|
+
if (kv) return kv[1] ?? null;
|
|
7267
|
+
}
|
|
7268
|
+
return null;
|
|
7269
|
+
}
|
|
7270
|
+
function readRequiredChecks(repoDir) {
|
|
7271
|
+
try {
|
|
7272
|
+
const text = readFileSync12(path17.join(repoDir, ".caddis", "coordinator.md"), "utf8");
|
|
7273
|
+
const m2 = /```json[^\S\r\n]*\r?\n([\s\S]*?)\r?\n[^\S\r\n]*```/.exec(text);
|
|
7274
|
+
if (!m2) return null;
|
|
7275
|
+
const parsed = JSON.parse(m2[1] ?? "");
|
|
7276
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
7277
|
+
const req = parsed.required_checks;
|
|
7278
|
+
if (!Array.isArray(req) || !req.every((x2) => typeof x2 === "string")) return null;
|
|
7279
|
+
return req;
|
|
7280
|
+
} catch {
|
|
7281
|
+
return null;
|
|
7282
|
+
}
|
|
7283
|
+
}
|
|
7284
|
+
function requiredState(checks, required) {
|
|
7285
|
+
if (required === null) return "unknown";
|
|
7286
|
+
let pending = false;
|
|
7287
|
+
for (const name of required) {
|
|
7288
|
+
const c = checks.find((x2) => x2.name === name);
|
|
7289
|
+
if (!c || c.status !== "completed") pending = true;
|
|
7290
|
+
else if (c.conclusion && FAILED.has(c.conclusion)) return "fail";
|
|
7291
|
+
else if (!c.conclusion || !PASSED.has(c.conclusion)) pending = true;
|
|
7292
|
+
}
|
|
7293
|
+
return pending ? "pending" : "pass";
|
|
7294
|
+
}
|
|
7295
|
+
async function defaultToken(spawn2, account) {
|
|
7296
|
+
const gh = await findGh();
|
|
7297
|
+
if (!gh) return null;
|
|
7298
|
+
const r2 = await spawn2(gh, ["auth", "token", "-u", account], { timeoutMs: TOKEN_TIMEOUT_MS });
|
|
7299
|
+
const token = r2.stdout.trim();
|
|
7300
|
+
return r2.ok && token ? token : null;
|
|
7301
|
+
}
|
|
7302
|
+
function parseAuthStatus(text) {
|
|
7303
|
+
const accounts = [];
|
|
7304
|
+
let active = null;
|
|
7305
|
+
let current = null;
|
|
7306
|
+
for (const line2 of text.split(/\r?\n/)) {
|
|
7307
|
+
const m2 = /Logged in to (\S+) (?:account|as) (\S+)/.exec(line2);
|
|
7308
|
+
if (m2) {
|
|
7309
|
+
current = m2[1] === "github.com" ? m2[2] ?? null : null;
|
|
7310
|
+
if (current && !accounts.includes(current)) accounts.push(current);
|
|
7311
|
+
continue;
|
|
7312
|
+
}
|
|
7313
|
+
if (current && /Active account:\s*true/i.test(line2)) active = current;
|
|
7314
|
+
}
|
|
7315
|
+
if (!active && accounts.length === 1) active = accounts[0] ?? null;
|
|
7316
|
+
return { active, accounts };
|
|
7317
|
+
}
|
|
7318
|
+
async function defaultAuthStatus(spawn2) {
|
|
7319
|
+
const gh = await findGh();
|
|
7320
|
+
if (!gh) return null;
|
|
7321
|
+
const r2 = await spawn2(gh, ["auth", "status", "-h", "github.com"], { timeoutMs: TOKEN_TIMEOUT_MS });
|
|
7322
|
+
if (r2.timedOut) return null;
|
|
7323
|
+
const status5 = parseAuthStatus(`${r2.stdout}
|
|
7324
|
+
${r2.stderr}`);
|
|
7325
|
+
return status5.accounts.length || r2.ok ? status5 : null;
|
|
7326
|
+
}
|
|
7327
|
+
function accountCandidates(owner, status5, known) {
|
|
7328
|
+
if (!status5) return [known ?? owner];
|
|
7329
|
+
const out = [];
|
|
7330
|
+
const add = (a) => {
|
|
7331
|
+
if (a && !out.includes(a)) out.push(a);
|
|
7332
|
+
};
|
|
7333
|
+
if (known && status5.accounts.includes(known)) add(known);
|
|
7334
|
+
add(status5.accounts.find((a) => a.toLowerCase() === owner.toLowerCase()));
|
|
7335
|
+
add(status5.active);
|
|
7336
|
+
status5.accounts.forEach(add);
|
|
7337
|
+
return out;
|
|
7338
|
+
}
|
|
7339
|
+
function accessError(owner, tried) {
|
|
7340
|
+
const set = `set "accounts": {"${owner}": "<gh user>"} in ~/.caddis/hub/config.json`;
|
|
7341
|
+
if (!tried.length) return `no gh account is logged in: run gh auth login -h github.com, then ${set}`;
|
|
7342
|
+
return `no access with account ${tried.join(", ")}: run gh auth login -h github.com, or ${set}`;
|
|
7343
|
+
}
|
|
7344
|
+
function createGithubSource(opts) {
|
|
7345
|
+
const now = opts.now ?? Date.now;
|
|
7346
|
+
const doFetch = opts.fetch ?? ((url, init2) => fetch(url, init2));
|
|
7347
|
+
const spawn2 = opts.spawn ?? spawnBounded;
|
|
7348
|
+
const getToken = opts.getToken ?? ((account) => defaultToken(spawn2, account));
|
|
7349
|
+
const authStatus = opts.authStatus ?? (opts.getToken ? async () => null : () => defaultAuthStatus(spawn2));
|
|
7350
|
+
const ownerAccount = /* @__PURE__ */ new Map();
|
|
7351
|
+
let etags = /* @__PURE__ */ new Map();
|
|
7352
|
+
let seen = /* @__PURE__ */ new Set();
|
|
7353
|
+
const slots = /* @__PURE__ */ new Map();
|
|
7354
|
+
const rate = /* @__PURE__ */ new Map();
|
|
7355
|
+
let lastOkAt = null;
|
|
7356
|
+
let lastError = null;
|
|
7357
|
+
let lastRunOk = false;
|
|
7358
|
+
let running = null;
|
|
7359
|
+
const noteRate = (account, res) => {
|
|
7360
|
+
const left = res.headers.get("x-ratelimit-remaining");
|
|
7361
|
+
const remaining = Number(left);
|
|
7362
|
+
if (left === null || !Number.isFinite(remaining)) return;
|
|
7363
|
+
const reset = Number(res.headers.get("x-ratelimit-reset"));
|
|
7364
|
+
rate.set(account, {
|
|
7365
|
+
remaining,
|
|
7366
|
+
reset_at: Number.isFinite(reset) && reset > 0 ? new Date(reset * 1e3).toISOString() : null
|
|
7367
|
+
});
|
|
7368
|
+
};
|
|
7369
|
+
const getJson = async (account, token, pathAndQuery) => {
|
|
7370
|
+
const url = `${API}${pathAndQuery}`;
|
|
7371
|
+
const key = `${account}
|
|
7372
|
+
${url}`;
|
|
7373
|
+
seen.add(key);
|
|
7374
|
+
const cached2 = etags.get(key);
|
|
7375
|
+
const headers = {
|
|
7376
|
+
Authorization: `Bearer ${token}`,
|
|
7377
|
+
Accept: "application/vnd.github+json",
|
|
7378
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
7379
|
+
"User-Agent": "caddis-hub"
|
|
7380
|
+
};
|
|
7381
|
+
if (cached2) headers["If-None-Match"] = cached2.etag;
|
|
7382
|
+
const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
7383
|
+
const res = await withTimeout(doFetch(url, { headers, signal }), FETCH_TIMEOUT_MS + 1e3, "GitHub request");
|
|
7384
|
+
noteRate(account, res);
|
|
7385
|
+
if (res.status === 304) {
|
|
7386
|
+
if (cached2) return cached2.body;
|
|
7387
|
+
throw new HttpError(304, false, "GitHub answered 304 with no cached copy");
|
|
7388
|
+
}
|
|
7389
|
+
if (!res.ok) {
|
|
7390
|
+
const limited = (res.status === 403 || res.status === 429) && (res.headers.get("x-ratelimit-remaining") === "0" || res.headers.get("retry-after") !== null);
|
|
7391
|
+
throw new HttpError(res.status, limited, limited ? "GitHub rate limit reached" : `GitHub answered ${res.status}`);
|
|
7392
|
+
}
|
|
7393
|
+
const body = await withTimeout(res.json(), FETCH_TIMEOUT_MS + 1e3, "GitHub response");
|
|
7394
|
+
const etag = res.headers.get("etag");
|
|
7395
|
+
if (etag) etags.set(key, { etag, body });
|
|
7396
|
+
return body;
|
|
7397
|
+
};
|
|
7398
|
+
const loadRepo = async (repoPath, repo, account, token) => {
|
|
7399
|
+
const required = readRequiredChecks(repoPath);
|
|
7400
|
+
let first;
|
|
7401
|
+
try {
|
|
7402
|
+
first = await getJson(account, token, `/repos/${repo}/pulls?state=open&per_page=50`);
|
|
7403
|
+
} catch (error) {
|
|
7404
|
+
if (error instanceof HttpError && error.status === 404) throw new HttpError(404, false, error.message, true);
|
|
7405
|
+
throw error;
|
|
7406
|
+
}
|
|
7407
|
+
const pulls = list(first);
|
|
7408
|
+
const prs = [];
|
|
7409
|
+
for (const p2 of pulls) {
|
|
7410
|
+
const head = str2(rec(p2.head).sha) ?? "";
|
|
7411
|
+
const byName = /* @__PURE__ */ new Map();
|
|
7412
|
+
if (head) {
|
|
7413
|
+
const body = rec(await getJson(account, token, `/repos/${repo}/commits/${head}/check-runs?per_page=100`));
|
|
7414
|
+
for (const c of list(body.check_runs)) {
|
|
7415
|
+
const name = str2(c.name);
|
|
7416
|
+
if (!name) continue;
|
|
7417
|
+
byName.set(name, {
|
|
7418
|
+
name,
|
|
7419
|
+
status: str2(c.status) ?? "unknown",
|
|
7420
|
+
conclusion: str2(c.conclusion),
|
|
7421
|
+
required: required === null ? null : required.includes(name)
|
|
7422
|
+
});
|
|
7423
|
+
}
|
|
7424
|
+
}
|
|
7425
|
+
const checks = [...byName.values()];
|
|
7426
|
+
prs.push({
|
|
7427
|
+
number: Number(p2.number),
|
|
7428
|
+
title: str2(p2.title) ?? "",
|
|
7429
|
+
head_sha: head,
|
|
7430
|
+
author: str2(rec(p2.user).login),
|
|
7431
|
+
draft: p2.draft === true,
|
|
7432
|
+
updated_at: str2(p2.updated_at),
|
|
7433
|
+
url: str2(p2.html_url) ?? "",
|
|
7434
|
+
checks,
|
|
7435
|
+
required_state: requiredState(checks, required)
|
|
7436
|
+
});
|
|
7437
|
+
}
|
|
7438
|
+
const closed = list(
|
|
7439
|
+
await getJson(account, token, `/repos/${repo}/pulls?state=closed&sort=updated&direction=desc&per_page=20`)
|
|
7440
|
+
);
|
|
7441
|
+
const merged = closed.filter((p2) => str2(p2.merged_at) && str2(p2.merge_commit_sha)).sort((a, b3) => String(b3.merged_at).localeCompare(String(a.merged_at))).slice(0, MERGES_SHOWN);
|
|
7442
|
+
const merges = [];
|
|
7443
|
+
for (const p2 of merged) {
|
|
7444
|
+
const sha = str2(p2.merge_commit_sha) ?? "";
|
|
7445
|
+
const body = rec(await getJson(account, token, `/repos/${repo}/actions/runs?head_sha=${sha}&per_page=10`));
|
|
7446
|
+
merges.push({
|
|
7447
|
+
number: Number(p2.number),
|
|
7448
|
+
title: str2(p2.title) ?? "",
|
|
7449
|
+
merge_sha: sha,
|
|
7450
|
+
merged_at: str2(p2.merged_at) ?? "",
|
|
7451
|
+
runs: list(body.workflow_runs).map((r2) => ({
|
|
7452
|
+
name: str2(r2.name) ?? "",
|
|
7453
|
+
status: str2(r2.status) ?? "unknown",
|
|
7454
|
+
conclusion: str2(r2.conclusion),
|
|
7455
|
+
url: str2(r2.html_url) ?? ""
|
|
7456
|
+
}))
|
|
7457
|
+
});
|
|
7458
|
+
}
|
|
7459
|
+
return { path: repoPath, repo, account, prs, merges };
|
|
7460
|
+
};
|
|
7461
|
+
const run2 = async () => {
|
|
7462
|
+
seen = /* @__PURE__ */ new Set();
|
|
7463
|
+
const limitedAccounts = /* @__PURE__ */ new Set();
|
|
7464
|
+
try {
|
|
7465
|
+
await runRepos(limitedAccounts);
|
|
7466
|
+
} finally {
|
|
7467
|
+
const kept = /* @__PURE__ */ new Map();
|
|
7468
|
+
for (const key of seen) {
|
|
7469
|
+
const entry = etags.get(key);
|
|
7470
|
+
if (entry) kept.set(key, entry);
|
|
7471
|
+
}
|
|
7472
|
+
etags = kept;
|
|
7473
|
+
}
|
|
7474
|
+
};
|
|
7475
|
+
const runRepos = async (limitedAccounts) => {
|
|
7476
|
+
const accounts = opts.accounts();
|
|
7477
|
+
const wanted = opts.repos();
|
|
7478
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
7479
|
+
const errors = [];
|
|
7480
|
+
let loaded = false;
|
|
7481
|
+
let status5;
|
|
7482
|
+
const tokenFor = async (account) => {
|
|
7483
|
+
if (!tokens.has(account)) {
|
|
7484
|
+
tokens.set(account, await withTimeout(getToken(account), TOKEN_TIMEOUT_MS + 5e3, "gh auth token"));
|
|
7485
|
+
}
|
|
7486
|
+
return tokens.get(account) ?? null;
|
|
7487
|
+
};
|
|
7488
|
+
for (const key of [...slots.keys()]) if (!wanted.includes(key)) slots.delete(key);
|
|
7489
|
+
for (const repoPath of wanted) {
|
|
7490
|
+
const url = readOriginUrl(repoPath);
|
|
7491
|
+
const repo = url ? parseGithubRemote(url) : null;
|
|
7492
|
+
if (!repo) {
|
|
7493
|
+
slots.set(repoPath, { path: repoPath, reason: "no github remote" });
|
|
7494
|
+
continue;
|
|
7495
|
+
}
|
|
7496
|
+
const owner = repo.split("/")[0] ?? "";
|
|
7497
|
+
const override = accounts[repo] ?? accounts[owner];
|
|
7498
|
+
let candidates;
|
|
7499
|
+
if (override) {
|
|
7500
|
+
candidates = [override];
|
|
7501
|
+
} else {
|
|
7502
|
+
if (status5 === void 0) {
|
|
7503
|
+
status5 = await withTimeout(authStatus(), TOKEN_TIMEOUT_MS + 5e3, "gh auth status").catch(() => null);
|
|
7504
|
+
}
|
|
7505
|
+
candidates = accountCandidates(owner, status5, ownerAccount.get(owner));
|
|
7506
|
+
}
|
|
7507
|
+
const old = slots.get(repoPath);
|
|
7508
|
+
const tried = [];
|
|
7509
|
+
let settled = false;
|
|
7510
|
+
for (const account of candidates) {
|
|
7511
|
+
if (limitedAccounts.has(account)) {
|
|
7512
|
+
settled = true;
|
|
7513
|
+
break;
|
|
7514
|
+
}
|
|
7515
|
+
tried.push(account);
|
|
7516
|
+
try {
|
|
7517
|
+
const token = await tokenFor(account);
|
|
7518
|
+
if (!token) continue;
|
|
7519
|
+
const slot = await loadRepo(repoPath, repo, account, token);
|
|
7520
|
+
slots.set(repoPath, { ...slot, last_ok_at: new Date(now()).toISOString() });
|
|
7521
|
+
if (!override) ownerAccount.set(owner, account);
|
|
7522
|
+
loaded = true;
|
|
7523
|
+
settled = true;
|
|
7524
|
+
break;
|
|
7525
|
+
} catch (error) {
|
|
7526
|
+
if (error instanceof HttpError && error.access && !error.rateLimited) continue;
|
|
7527
|
+
if (error instanceof HttpError && error.rateLimited) limitedAccounts.add(account);
|
|
7528
|
+
const message3 = error instanceof Error ? error.message : String(error);
|
|
7529
|
+
errors.push(`${repo}: ${message3}`);
|
|
7530
|
+
if (old?.prs !== void 0) {
|
|
7531
|
+
if (!(error instanceof HttpError && error.rateLimited)) slots.set(repoPath, { ...old, error: message3 });
|
|
7532
|
+
} else {
|
|
7533
|
+
slots.set(repoPath, { path: repoPath, repo, account, error: message3 });
|
|
7534
|
+
}
|
|
7535
|
+
settled = true;
|
|
7536
|
+
break;
|
|
7537
|
+
}
|
|
7538
|
+
}
|
|
7539
|
+
if (!settled) {
|
|
7540
|
+
if (!override && ownerAccount.get(owner) !== void 0) ownerAccount.delete(owner);
|
|
7541
|
+
const slot = { path: repoPath, repo, error: accessError(owner, tried) };
|
|
7542
|
+
if (tried[0]) slot.account = tried[0];
|
|
7543
|
+
if (old?.last_ok_at) slot.last_ok_at = old.last_ok_at;
|
|
7544
|
+
slots.set(repoPath, slot);
|
|
7545
|
+
}
|
|
7546
|
+
}
|
|
7547
|
+
lastRunOk = errors.length === 0;
|
|
7548
|
+
lastError = errors.length ? errors.join("; ") : null;
|
|
7549
|
+
if (lastRunOk || loaded) lastOkAt = now();
|
|
7550
|
+
};
|
|
7551
|
+
return {
|
|
7552
|
+
refresh() {
|
|
7553
|
+
if (!running) {
|
|
7554
|
+
running = run2().catch((error) => {
|
|
7555
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
7556
|
+
lastRunOk = false;
|
|
7557
|
+
}).finally(() => {
|
|
7558
|
+
running = null;
|
|
7559
|
+
});
|
|
7560
|
+
}
|
|
7561
|
+
return running;
|
|
7562
|
+
},
|
|
7563
|
+
get() {
|
|
7564
|
+
return {
|
|
7565
|
+
ok: lastRunOk,
|
|
7566
|
+
last_ok_at: lastOkAt === null ? null : new Date(lastOkAt).toISOString(),
|
|
7567
|
+
last_error: lastError,
|
|
7568
|
+
stale: lastOkAt === null || now() - lastOkAt > STALE_AFTER_MS4,
|
|
7569
|
+
data: lastOkAt === null && slots.size === 0 ? null : { repos: [...slots.values()], rate: Object.fromEntries(rate) }
|
|
7570
|
+
};
|
|
7571
|
+
},
|
|
7572
|
+
nextDelayMs() {
|
|
7573
|
+
for (const r2 of rate.values()) if (r2.remaining < RATE_RESERVE) return SLOW_POLL_MS;
|
|
7574
|
+
return POLL_MS2;
|
|
7575
|
+
}
|
|
7576
|
+
};
|
|
7577
|
+
}
|
|
7578
|
+
var STALE_AFTER_MS4, POLL_MS2, SLOW_POLL_MS, RATE_RESERVE, API, TOKEN_TIMEOUT_MS, FIND_TIMEOUT_MS3, FETCH_TIMEOUT_MS, MERGES_SHOWN, FAILED, PASSED, HttpError, str2, rec, list, findGh;
|
|
7579
|
+
var init_github = __esm({
|
|
7580
|
+
"src/hub/sources/github.ts"() {
|
|
7581
|
+
"use strict";
|
|
7582
|
+
init_which();
|
|
7583
|
+
init_bounded();
|
|
7584
|
+
init_spawn();
|
|
7585
|
+
init_agents();
|
|
7586
|
+
STALE_AFTER_MS4 = 3 * 6e4;
|
|
7587
|
+
POLL_MS2 = 6e4;
|
|
7588
|
+
SLOW_POLL_MS = 5 * 6e4;
|
|
7589
|
+
RATE_RESERVE = 500;
|
|
7590
|
+
API = "https://api.github.com";
|
|
7591
|
+
TOKEN_TIMEOUT_MS = 1e4;
|
|
7592
|
+
FIND_TIMEOUT_MS3 = 1e4;
|
|
7593
|
+
FETCH_TIMEOUT_MS = 15e3;
|
|
7594
|
+
MERGES_SHOWN = 5;
|
|
7595
|
+
FAILED = /* @__PURE__ */ new Set(["failure", "cancelled", "timed_out", "action_required", "startup_failure", "stale"]);
|
|
7596
|
+
PASSED = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
|
|
7597
|
+
HttpError = class extends Error {
|
|
7598
|
+
constructor(status5, rateLimited, message3, access = false) {
|
|
7599
|
+
super(message3);
|
|
7600
|
+
this.status = status5;
|
|
7601
|
+
this.rateLimited = rateLimited;
|
|
7602
|
+
this.access = access || status5 === 401 || status5 === 403 && !rateLimited;
|
|
7603
|
+
}
|
|
7604
|
+
status;
|
|
7605
|
+
rateLimited;
|
|
7606
|
+
/** This token cannot see the repo: try the next account. */
|
|
7607
|
+
access;
|
|
7608
|
+
};
|
|
7609
|
+
str2 = (v2) => typeof v2 === "string" && v2 ? v2 : null;
|
|
7610
|
+
rec = (v2) => v2 && typeof v2 === "object" ? v2 : {};
|
|
7611
|
+
list = (v2) => Array.isArray(v2) ? v2.map(rec) : [];
|
|
7612
|
+
findGh = () => withTimeout(findBin("gh"), FIND_TIMEOUT_MS3, "finding gh");
|
|
7613
|
+
}
|
|
7614
|
+
});
|
|
7615
|
+
|
|
7616
|
+
// src/hub/state.ts
|
|
7617
|
+
import { createHash as createHash2 } from "crypto";
|
|
7618
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
7619
|
+
import path18 from "path";
|
|
7620
|
+
function repoIdFor(nameWithOwner) {
|
|
7621
|
+
return nameWithOwner.trim().toLowerCase().replace(/\//g, "__");
|
|
7622
|
+
}
|
|
7623
|
+
function localRepoId(repoPath) {
|
|
7624
|
+
const norm = process.platform === "win32" ? repoPath.toLowerCase().replace(/\//g, "\\") : repoPath;
|
|
7625
|
+
const digest = createHash2("sha256").update(norm, "utf8").digest("hex").slice(0, 6);
|
|
7626
|
+
return `local__${path18.basename(repoPath)}__${digest}`;
|
|
7627
|
+
}
|
|
7628
|
+
function readPluginVersion(home) {
|
|
7629
|
+
try {
|
|
7630
|
+
const file = path18.join(home, ".claude", "plugins", "installed_plugins.json");
|
|
7631
|
+
const parsed = JSON.parse(readFileSync13(file, "utf8"));
|
|
7632
|
+
for (const [key, entries] of Object.entries(parsed.plugins ?? {})) {
|
|
7633
|
+
if (!key.startsWith("caddis@") || !Array.isArray(entries)) continue;
|
|
7634
|
+
for (const e2 of entries) {
|
|
7635
|
+
const v2 = e2?.version;
|
|
7636
|
+
if (typeof v2 === "string" && v2) return v2;
|
|
7637
|
+
}
|
|
7638
|
+
}
|
|
7639
|
+
} catch {
|
|
7640
|
+
}
|
|
7641
|
+
return null;
|
|
7642
|
+
}
|
|
7643
|
+
function defaultVersions(home) {
|
|
7644
|
+
const pool = bundleManifest().poolVersion;
|
|
7645
|
+
return { hub: packageInfo().version, pool: pool === "unknown" ? null : pool, plugin: readPluginVersion(home) };
|
|
7646
|
+
}
|
|
7647
|
+
function createHub(opts = {}) {
|
|
7648
|
+
const home = opts.home ?? caddisHome();
|
|
7649
|
+
const now = opts.now ?? Date.now;
|
|
7650
|
+
const repoPaths = () => {
|
|
7651
|
+
const cfg = readConfig(home).value;
|
|
7652
|
+
const hidden = new Set(cfg.hidden.map((p2) => repoKey(normalizeRepoPath(p2))));
|
|
7653
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7654
|
+
const out = [];
|
|
7655
|
+
const add = (p2) => {
|
|
7656
|
+
const n = normalizeRepoPath(p2);
|
|
7657
|
+
const k3 = repoKey(n);
|
|
7658
|
+
if (hidden.has(k3) || seen.has(k3)) return;
|
|
7659
|
+
seen.add(k3);
|
|
7660
|
+
out.push(n);
|
|
7661
|
+
};
|
|
7662
|
+
cfg.pinned.forEach(add);
|
|
7663
|
+
readRegistry(home).value.repos.forEach((r2) => add(r2.path));
|
|
7664
|
+
return out;
|
|
7665
|
+
};
|
|
7666
|
+
const collector = opts.collector ?? createCollectorSource({ repos: repoPaths, now });
|
|
7667
|
+
const agents = opts.agents ?? createAgentsSource({ now });
|
|
7668
|
+
const github = opts.github ?? createGithubSource({ repos: repoPaths, accounts: () => readConfig(home).value.accounts, now });
|
|
7669
|
+
const coordinator = opts.coordinator ?? createCoordinatorSource({ home, now });
|
|
7670
|
+
let versionsCache = null;
|
|
7671
|
+
const versions = () => {
|
|
7672
|
+
if (opts.versions) return opts.versions();
|
|
7673
|
+
if (!versionsCache || now() - versionsCache.at > VERSIONS_TTL_MS) {
|
|
7674
|
+
versionsCache = { at: now(), value: defaultVersions(home) };
|
|
7675
|
+
}
|
|
7676
|
+
return versionsCache.value;
|
|
7677
|
+
};
|
|
7678
|
+
const thrown = /* @__PURE__ */ new Map();
|
|
7679
|
+
let coordinatorOkAt = null;
|
|
7680
|
+
const guarded = async (name, run2) => {
|
|
7681
|
+
try {
|
|
7682
|
+
await withTimeout(Promise.resolve().then(run2), REFRESH_CEILING_MS[name], "refresh");
|
|
7683
|
+
thrown.delete(name);
|
|
7684
|
+
if (name === "coordinator") coordinatorOkAt = now();
|
|
7685
|
+
} catch (e2) {
|
|
7686
|
+
thrown.set(name, message(e2));
|
|
7687
|
+
}
|
|
7688
|
+
};
|
|
7689
|
+
const registerSessionRepos = () => {
|
|
7690
|
+
const sessions = agents.get().data ?? [];
|
|
7691
|
+
const paths = sessions.map((s) => s.repo_path).filter((p2) => typeof p2 === "string" && p2 !== "");
|
|
7692
|
+
if (!paths.length) return;
|
|
7693
|
+
try {
|
|
7694
|
+
addRepos(paths, home);
|
|
7695
|
+
} catch {
|
|
7696
|
+
}
|
|
7697
|
+
};
|
|
7698
|
+
const refreshers = {
|
|
7699
|
+
agents: () => guarded("agents", async () => {
|
|
7700
|
+
await agents.refresh();
|
|
7701
|
+
registerSessionRepos();
|
|
7702
|
+
}),
|
|
7703
|
+
collector: () => guarded("collector", () => collector.refresh()),
|
|
7704
|
+
github: () => guarded("github", () => github.refresh()),
|
|
7705
|
+
coordinator: () => guarded("coordinator", () => coordinator.refresh())
|
|
7706
|
+
};
|
|
7707
|
+
const delays = {
|
|
7708
|
+
agents: () => AGENTS_EVERY_MS,
|
|
7709
|
+
collector: () => COLLECTOR_EVERY_MS,
|
|
7710
|
+
github: () => github.nextDelayMs(),
|
|
7711
|
+
coordinator: () => COORDINATOR_EVERY_MS
|
|
7712
|
+
};
|
|
7713
|
+
let running = false;
|
|
7714
|
+
const timers = /* @__PURE__ */ new Map();
|
|
7715
|
+
const schedule = (name, delay) => {
|
|
7716
|
+
if (!running) return;
|
|
7717
|
+
const timer = setTimeout(() => {
|
|
7718
|
+
timers.delete(name);
|
|
7719
|
+
void refreshers[name]().finally(() => {
|
|
7720
|
+
let next = AGENTS_EVERY_MS;
|
|
7721
|
+
try {
|
|
7722
|
+
next = delays[name]();
|
|
7723
|
+
} catch {
|
|
7724
|
+
}
|
|
7725
|
+
schedule(name, next);
|
|
7726
|
+
});
|
|
7727
|
+
}, delay);
|
|
7728
|
+
timer.unref();
|
|
7729
|
+
timers.set(name, timer);
|
|
7730
|
+
};
|
|
7731
|
+
const status5 = (name, s) => {
|
|
7732
|
+
const err = thrown.get(name);
|
|
7733
|
+
const okAt = s.last_ok_at === null ? null : Date.parse(s.last_ok_at);
|
|
7734
|
+
return {
|
|
7735
|
+
ok: s.ok && err === void 0,
|
|
7736
|
+
last_ok_at: s.last_ok_at,
|
|
7737
|
+
last_error: err ?? s.last_error,
|
|
7738
|
+
stale: okAt === null || Number.isNaN(okAt) || now() - okAt > STALE_AFTER_MS5
|
|
7739
|
+
};
|
|
7740
|
+
};
|
|
7741
|
+
const getState = () => {
|
|
7742
|
+
const col = collector.get();
|
|
7743
|
+
const ag = agents.get();
|
|
7744
|
+
const gh = github.get();
|
|
7745
|
+
const coord = coordinator.get();
|
|
7746
|
+
const pinned = new Set(readConfig(home).value.pinned.map((p2) => repoKey(normalizeRepoPath(p2))));
|
|
7747
|
+
const colByPath = new Map((col.data?.repos ?? []).map((r2) => [repoKey(r2.path), r2]));
|
|
7748
|
+
const ghByPath = new Map((gh.data?.repos ?? []).map((r2) => [repoKey(r2.path), r2]));
|
|
7749
|
+
const coordById = new Map(coord.map((c) => [c.repo_id, c]));
|
|
7750
|
+
const repos = repoPaths().map((p2) => {
|
|
7751
|
+
const k3 = repoKey(p2);
|
|
7752
|
+
const c = colByPath.get(k3);
|
|
7753
|
+
const g2 = ghByPath.get(k3) ?? null;
|
|
7754
|
+
const repoId = g2?.repo ? repoIdFor(g2.repo) : g2?.reason === "no github remote" ? localRepoId(p2) : void 0;
|
|
7755
|
+
const out = {
|
|
7756
|
+
path: p2,
|
|
7757
|
+
name: path18.basename(p2),
|
|
7758
|
+
pinned: pinned.has(k3),
|
|
7759
|
+
roadmaps: c?.roadmaps ?? null,
|
|
7760
|
+
plans: c?.plans ?? null,
|
|
7761
|
+
relays: c?.relays ?? null,
|
|
7762
|
+
github: g2,
|
|
7763
|
+
coordinator: repoId ? coordById.get(repoId) ?? null : null
|
|
7764
|
+
};
|
|
7765
|
+
if (repoId) out.repo_id = repoId;
|
|
7766
|
+
if (c?.error !== void 0) out.error = c.error;
|
|
7767
|
+
return out;
|
|
7768
|
+
});
|
|
7769
|
+
const coordOkAt = coordinatorOkAt === null ? null : new Date(coordinatorOkAt).toISOString();
|
|
7770
|
+
return {
|
|
7771
|
+
schema: STATE_SCHEMA,
|
|
7772
|
+
generated_at: new Date(now()).toISOString(),
|
|
7773
|
+
versions: versions(),
|
|
7774
|
+
sources: {
|
|
7775
|
+
collector: status5("collector", col),
|
|
7776
|
+
agents: status5("agents", ag),
|
|
7777
|
+
github: status5("github", gh),
|
|
7778
|
+
coordinator: status5("coordinator", { ok: coordOkAt !== null, last_ok_at: coordOkAt, last_error: null })
|
|
7779
|
+
},
|
|
7780
|
+
update_needed: col.update_needed || coord.some((c) => c.update_needed),
|
|
7781
|
+
repos,
|
|
7782
|
+
sessions: ag.data ?? [],
|
|
7783
|
+
spend: col.data?.spend ?? null,
|
|
7784
|
+
guide: col.data?.guide ?? null
|
|
7785
|
+
};
|
|
7786
|
+
};
|
|
7787
|
+
return {
|
|
7788
|
+
start() {
|
|
7789
|
+
if (running) return;
|
|
7790
|
+
running = true;
|
|
7791
|
+
schedule("coordinator", 0);
|
|
7792
|
+
const first = setTimeout(() => {
|
|
7793
|
+
timers.delete("agents");
|
|
7794
|
+
void refreshers.agents().finally(() => {
|
|
7795
|
+
schedule("agents", AGENTS_EVERY_MS);
|
|
7796
|
+
schedule("collector", 0);
|
|
7797
|
+
schedule("github", 0);
|
|
7798
|
+
});
|
|
7799
|
+
}, 0);
|
|
7800
|
+
first.unref();
|
|
7801
|
+
timers.set("agents", first);
|
|
7802
|
+
},
|
|
7803
|
+
stop() {
|
|
7804
|
+
running = false;
|
|
7805
|
+
for (const timer of timers.values()) clearTimeout(timer);
|
|
7806
|
+
timers.clear();
|
|
7807
|
+
},
|
|
7808
|
+
getState,
|
|
7809
|
+
async refreshAll() {
|
|
7810
|
+
await refreshers.agents();
|
|
7811
|
+
await refreshers.coordinator();
|
|
7812
|
+
await refreshers.collector();
|
|
7813
|
+
await refreshers.github();
|
|
7814
|
+
},
|
|
7815
|
+
repoPaths
|
|
7816
|
+
};
|
|
7817
|
+
}
|
|
7818
|
+
var STATE_SCHEMA, STALE_AFTER_MS5, COLLECTOR_EVERY_MS, AGENTS_EVERY_MS, COORDINATOR_EVERY_MS, VERSIONS_TTL_MS, REFRESH_CEILING_MS, message;
|
|
7819
|
+
var init_state = __esm({
|
|
7820
|
+
"src/hub/state.ts"() {
|
|
7821
|
+
"use strict";
|
|
7822
|
+
init_config();
|
|
7823
|
+
init_agents();
|
|
7824
|
+
init_collector();
|
|
7825
|
+
init_coordinator();
|
|
7826
|
+
init_github();
|
|
7827
|
+
init_bounded();
|
|
7828
|
+
init_pkg();
|
|
7829
|
+
init_sessions();
|
|
7830
|
+
STATE_SCHEMA = 1;
|
|
7831
|
+
STALE_AFTER_MS5 = 3 * 6e4;
|
|
7832
|
+
COLLECTOR_EVERY_MS = 6e4;
|
|
7833
|
+
AGENTS_EVERY_MS = 3e4;
|
|
7834
|
+
COORDINATOR_EVERY_MS = 3e4;
|
|
7835
|
+
VERSIONS_TTL_MS = 6e4;
|
|
7836
|
+
REFRESH_CEILING_MS = {
|
|
7837
|
+
collector: 9e4,
|
|
7838
|
+
agents: 9e4,
|
|
7839
|
+
github: 3e5,
|
|
7840
|
+
coordinator: 9e4
|
|
7841
|
+
};
|
|
7842
|
+
message = (e2) => e2 instanceof Error ? e2.message : String(e2);
|
|
7843
|
+
}
|
|
7844
|
+
});
|
|
7845
|
+
|
|
7846
|
+
// src/commands/hub.ts
|
|
7847
|
+
var hub_exports = {};
|
|
7848
|
+
__export(hub_exports, {
|
|
7849
|
+
SPAWN_OPTIONS: () => SPAWN_OPTIONS,
|
|
7850
|
+
START_WAIT_MS: () => START_WAIT_MS,
|
|
7851
|
+
hubAutostart: () => hubAutostart,
|
|
7852
|
+
hubOpen: () => hubOpen,
|
|
7853
|
+
hubServe: () => hubServe,
|
|
7854
|
+
hubStart: () => hubStart,
|
|
7855
|
+
hubStatus: () => hubStatus,
|
|
7856
|
+
hubStop: () => hubStop,
|
|
7857
|
+
isNodeProcess: () => isNodeProcess,
|
|
7858
|
+
killTree: () => killTree2,
|
|
7859
|
+
processStartMs: () => processStartMs,
|
|
7860
|
+
startHub: () => startHub
|
|
7861
|
+
});
|
|
7862
|
+
import http2 from "http";
|
|
7863
|
+
import path19 from "path";
|
|
7864
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync14 } from "fs";
|
|
7865
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
7866
|
+
function defaultVersions2() {
|
|
7867
|
+
return { cli: packageInfo().version, caddis: bundleManifest().poolVersion };
|
|
7868
|
+
}
|
|
7869
|
+
function resolvePort(deps) {
|
|
7870
|
+
if (deps.port !== void 0) return deps.port;
|
|
7871
|
+
const cfg = readConfig(deps.home);
|
|
7872
|
+
if (cfg.error) errorLine(`${cfg.error}; using the last good config`);
|
|
7873
|
+
const p2 = cfg.value.port;
|
|
7874
|
+
if (!Number.isInteger(p2) || p2 < 1 || p2 > 65535) {
|
|
7875
|
+
throw new HubError(`config.json port ${p2} is not valid: use a whole number in 1-65535`);
|
|
7876
|
+
}
|
|
7877
|
+
return p2;
|
|
7878
|
+
}
|
|
7879
|
+
function readKeyQuiet(home) {
|
|
7880
|
+
try {
|
|
7881
|
+
const k3 = readFileSync14(keyPath(home), "utf8").trim();
|
|
7882
|
+
return /^[0-9a-f]{64}$/.test(k3) ? k3 : null;
|
|
7883
|
+
} catch {
|
|
7884
|
+
return null;
|
|
7885
|
+
}
|
|
7886
|
+
}
|
|
7887
|
+
function hubRequest(method, port, p2, key, timeoutMs) {
|
|
7888
|
+
return new Promise((resolve) => {
|
|
7889
|
+
const req = http2.request(
|
|
7890
|
+
{
|
|
7891
|
+
host: "127.0.0.1",
|
|
7892
|
+
port,
|
|
7893
|
+
method,
|
|
7894
|
+
path: p2,
|
|
7895
|
+
headers: { host: `127.0.0.1:${port}`, "content-length": 0, ...key ? { [KEY_HEADER]: key } : {} },
|
|
7896
|
+
agent: false,
|
|
7897
|
+
timeout: timeoutMs
|
|
7898
|
+
},
|
|
7899
|
+
(res) => {
|
|
7900
|
+
const chunks = [];
|
|
7901
|
+
res.on("data", (c) => chunks.push(c));
|
|
7902
|
+
res.on("end", () => {
|
|
7903
|
+
let body = {};
|
|
7904
|
+
try {
|
|
7905
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
7906
|
+
} catch {
|
|
7907
|
+
}
|
|
7908
|
+
resolve({ status: res.statusCode ?? 0, body });
|
|
7909
|
+
});
|
|
7910
|
+
res.on("error", () => resolve(null));
|
|
7911
|
+
}
|
|
7912
|
+
);
|
|
7913
|
+
req.on("timeout", () => req.destroy());
|
|
7914
|
+
req.on("error", () => resolve(null));
|
|
7915
|
+
req.end();
|
|
7916
|
+
});
|
|
7917
|
+
}
|
|
7918
|
+
async function defaultSpawn(cmd, args, opts = {}) {
|
|
7919
|
+
const { execa } = await import("execa");
|
|
7920
|
+
const child = execa(cmd, args, { ...SPAWN_OPTIONS, cwd: opts.cwd });
|
|
7921
|
+
child.unref();
|
|
7922
|
+
return {
|
|
7923
|
+
pid: child.pid,
|
|
7924
|
+
exited: child.then(
|
|
7925
|
+
(r2) => r2.exitCode ?? null,
|
|
7926
|
+
() => null
|
|
7927
|
+
)
|
|
7928
|
+
};
|
|
7929
|
+
}
|
|
7930
|
+
async function killTree2(pid) {
|
|
7931
|
+
if (process.platform === "win32") {
|
|
7932
|
+
await run("taskkill", ["/T", "/F", "/PID", String(pid)], { timeout: TOOL_TIMEOUT_MS });
|
|
7933
|
+
return;
|
|
7934
|
+
}
|
|
7935
|
+
try {
|
|
7936
|
+
process.kill(-pid, "SIGTERM");
|
|
7937
|
+
} catch {
|
|
7938
|
+
try {
|
|
7939
|
+
process.kill(pid, "SIGTERM");
|
|
7940
|
+
} catch {
|
|
7941
|
+
}
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
async function isNodeProcess(pid) {
|
|
7945
|
+
const r2 = process.platform === "win32" ? await run("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], { timeout: TOOL_TIMEOUT_MS }) : await run("ps", ["-p", String(pid), "-o", "comm="], { timeout: TOOL_TIMEOUT_MS });
|
|
7946
|
+
if (!r2.ok) return false;
|
|
7947
|
+
const name = (r2.stdout.split(/\r?\n/)[0] ?? "").replace(/^"([^"]*)".*$/, "$1").trim().toLowerCase();
|
|
7948
|
+
if (!name) return false;
|
|
7949
|
+
const runtime = path19.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
|
|
7950
|
+
return name.includes("node") || name.includes(runtime);
|
|
7951
|
+
}
|
|
7952
|
+
async function defaultOpener(url) {
|
|
7953
|
+
const r2 = process.platform === "win32" ? await run("cmd", ["/c", "start", "", url], { timeout: TOOL_TIMEOUT_MS }) : process.platform === "darwin" ? await run("open", [url], { timeout: TOOL_TIMEOUT_MS }) : await run("xdg-open", [url], { timeout: TOOL_TIMEOUT_MS });
|
|
7954
|
+
return r2.ok;
|
|
7955
|
+
}
|
|
7956
|
+
async function processStartMs(pid) {
|
|
7957
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
7958
|
+
const r2 = process.platform === "win32" ? await run(
|
|
7959
|
+
"powershell",
|
|
7960
|
+
["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().ToString('o')`],
|
|
7961
|
+
{ timeout: TOOL_TIMEOUT_MS }
|
|
7962
|
+
) : await run("ps", ["-o", "lstart=", "-p", String(pid)], { timeout: TOOL_TIMEOUT_MS, env: { LC_ALL: "C" } });
|
|
7963
|
+
if (!r2.ok) return null;
|
|
7964
|
+
const text = r2.stdout.trim().replace(/(\.\d{3})\d+/, "$1");
|
|
7965
|
+
const t = Date.parse(text);
|
|
7966
|
+
return Number.isNaN(t) ? null : t;
|
|
7967
|
+
}
|
|
7968
|
+
async function whoami(port) {
|
|
7969
|
+
const r2 = await hubRequest("GET", port, "/api/hub/whoami", null, WHOAMI_TIMEOUT_MS);
|
|
7970
|
+
const id = r2?.status === 200 ? r2.body.instance : void 0;
|
|
7971
|
+
return typeof id === "string" ? id : null;
|
|
7972
|
+
}
|
|
7973
|
+
async function sameProcess(lock, deps) {
|
|
7974
|
+
const home = deps.home ?? caddisHome();
|
|
7975
|
+
if (lock.pid === process.pid) return holderAlive(lock, home, () => false) ? "same" : "other";
|
|
7976
|
+
const started = Date.parse(lock.started_at);
|
|
7977
|
+
if (Number.isNaN(started)) return "other";
|
|
7978
|
+
const t = await (deps.processStart ?? processStartMs)(lock.pid);
|
|
7979
|
+
if (t === null) return "unknown";
|
|
7980
|
+
return t <= started ? "same" : "other";
|
|
7981
|
+
}
|
|
7982
|
+
async function inspect(deps, lock = readLock(deps.home ?? caddisHome())) {
|
|
7983
|
+
const home = deps.home ?? caddisHome();
|
|
7984
|
+
if (!lock) return { kind: "none" };
|
|
7985
|
+
if (!holderAlive(lock, home, deps.alive ?? pidAlive)) return { kind: "stale", lock };
|
|
7986
|
+
if (lock.instance && lock.port > 0 && await whoami(lock.port) === lock.instance) return { kind: "running", lock };
|
|
7987
|
+
if (await sameProcess(lock, deps) === "other") return { kind: "stale", lock };
|
|
7988
|
+
const started = Date.parse(lock.started_at);
|
|
7989
|
+
const young = !Number.isNaN(started) && (deps.now ?? Date.now)() - started < START_WAIT_MS;
|
|
7990
|
+
return young ? { kind: "starting", lock } : { kind: "unresponsive", lock };
|
|
7991
|
+
}
|
|
7992
|
+
async function startHub(deps = {}) {
|
|
7993
|
+
const home = deps.home ?? caddisHome();
|
|
7994
|
+
const now = deps.now ?? Date.now;
|
|
7995
|
+
const port = resolvePort(deps);
|
|
7996
|
+
const info = {
|
|
7997
|
+
pid: process.pid,
|
|
7998
|
+
started_at: new Date(now()).toISOString(),
|
|
7999
|
+
port,
|
|
8000
|
+
instance: randomBytes4(16).toString("hex")
|
|
8001
|
+
};
|
|
8002
|
+
let got = acquireLock(info, home, deps.alive ?? pidAlive);
|
|
8003
|
+
if (!got.ok) {
|
|
8004
|
+
const st = await inspect(deps, got.holder);
|
|
8005
|
+
if (st.kind === "running" || st.kind === "starting" || st.kind === "unresponsive") throw new HubError(refusal(st));
|
|
8006
|
+
removeLockIf(got.holder, home);
|
|
8007
|
+
got = acquireLock(info, home, deps.alive ?? pidAlive);
|
|
8008
|
+
if (!got.ok) throw new HubError(alreadyRunning(got.holder));
|
|
8009
|
+
}
|
|
8010
|
+
let server;
|
|
8011
|
+
const state = deps.state ?? createHub({ home, now });
|
|
8012
|
+
let stopping = null;
|
|
8013
|
+
let resolveDone;
|
|
8014
|
+
const done = new Promise((r2) => resolveDone = r2);
|
|
8015
|
+
const stop = () => {
|
|
8016
|
+
stopping ??= (async () => {
|
|
8017
|
+
try {
|
|
8018
|
+
state.stop();
|
|
8019
|
+
await server.close();
|
|
8020
|
+
} finally {
|
|
8021
|
+
releaseLock(process.pid, home);
|
|
8022
|
+
resolveDone();
|
|
8023
|
+
}
|
|
8024
|
+
})();
|
|
8025
|
+
return stopping;
|
|
8026
|
+
};
|
|
8027
|
+
try {
|
|
8028
|
+
const key = await ensureKey({ home, runCmd: deps.runCmd });
|
|
8029
|
+
server = await startServer({
|
|
8030
|
+
host: "127.0.0.1",
|
|
8031
|
+
port,
|
|
8032
|
+
key,
|
|
8033
|
+
getState: () => state.getState(),
|
|
8034
|
+
versions: deps.versions ?? defaultVersions2(),
|
|
8035
|
+
now,
|
|
8036
|
+
instance: info.instance,
|
|
8037
|
+
onStop: () => void stop()
|
|
8038
|
+
});
|
|
8039
|
+
} catch (e2) {
|
|
8040
|
+
releaseLock(process.pid, home);
|
|
8041
|
+
if (e2.code === "EADDRINUSE") {
|
|
8042
|
+
throw new HubError(`port ${port} is in use by another program: free it or set "port" in the hub config.json`);
|
|
8043
|
+
}
|
|
8044
|
+
throw e2;
|
|
8045
|
+
}
|
|
8046
|
+
writeLock({ ...info, port: server.port }, home);
|
|
8047
|
+
state.start();
|
|
8048
|
+
return { port: server.port, stop, done };
|
|
8049
|
+
}
|
|
8050
|
+
async function killChild(child, deps) {
|
|
8051
|
+
if (child.pid && (deps.alive ?? pidAlive)(child.pid)) await (deps.kill ?? killTree2)(child.pid);
|
|
8052
|
+
}
|
|
8053
|
+
async function hubServe(deps = {}) {
|
|
8054
|
+
const home = deps.home ?? caddisHome();
|
|
8055
|
+
let hub2;
|
|
8056
|
+
try {
|
|
8057
|
+
hub2 = await startHub(deps);
|
|
8058
|
+
} catch (e2) {
|
|
8059
|
+
errorLine(message2(e2));
|
|
8060
|
+
return 1;
|
|
8061
|
+
}
|
|
8062
|
+
let restarting = false;
|
|
8063
|
+
let exitCode = 0;
|
|
8064
|
+
let resolveFinished;
|
|
8065
|
+
const finished = new Promise((r2) => resolveFinished = r2);
|
|
8066
|
+
const watch = (h2) => {
|
|
8067
|
+
void h2.done.then(() => {
|
|
8068
|
+
if (!restarting) resolveFinished();
|
|
8069
|
+
});
|
|
8070
|
+
};
|
|
8071
|
+
watch(hub2);
|
|
8072
|
+
const onSignal = () => void hub2.stop();
|
|
8073
|
+
const onExit = () => releaseLock(process.pid, home);
|
|
8074
|
+
process.once("SIGINT", onSignal);
|
|
8075
|
+
process.once("SIGTERM", onSignal);
|
|
8076
|
+
process.once("exit", onExit);
|
|
8077
|
+
line(`caddis hub serving on ${hubUrl(hub2.port)} (pid ${process.pid}); stop it with: caddis hub stop`);
|
|
8078
|
+
const pkgRoot = packageInfo().root;
|
|
8079
|
+
const newEntry = path19.join(pkgRoot, "dist", "cli.js");
|
|
8080
|
+
const runningVersion = (deps.versions ?? defaultVersions2()).cli ?? packageInfo().version;
|
|
8081
|
+
const restarter = createRestarter({
|
|
8082
|
+
currentVersion: runningVersion,
|
|
8083
|
+
readVersion: () => readInstalledVersion2(pkgRoot),
|
|
8084
|
+
closeServer: async () => {
|
|
8085
|
+
restarting = true;
|
|
8086
|
+
await hub2.stop();
|
|
8087
|
+
},
|
|
8088
|
+
spawnNew: () => (deps.spawn ?? defaultSpawn)(process.execPath, [newEntry, "hub", "serve"], { cwd: hubDir(home) }),
|
|
8089
|
+
probe: async (child) => {
|
|
8090
|
+
const lock = readLock(home);
|
|
8091
|
+
const key = readKeyQuiet(home);
|
|
8092
|
+
if (!lock || !key || lock.pid !== child.pid || lock.port <= 0 || !lock.instance) return null;
|
|
8093
|
+
if (await whoami(lock.port) !== lock.instance) return null;
|
|
8094
|
+
const h2 = await hubRequest("GET", lock.port, "/api/health", key, 1e3);
|
|
8095
|
+
const v2 = h2?.body.versions?.cli;
|
|
8096
|
+
return h2?.status === 200 && typeof v2 === "string" ? v2 : null;
|
|
8097
|
+
},
|
|
8098
|
+
restartOwn: async () => {
|
|
8099
|
+
try {
|
|
8100
|
+
hub2 = await startHub({ ...deps, port: hub2.port });
|
|
8101
|
+
watch(hub2);
|
|
8102
|
+
} finally {
|
|
8103
|
+
restarting = false;
|
|
8104
|
+
}
|
|
8105
|
+
},
|
|
8106
|
+
killChild: (child) => killChild(child, deps),
|
|
8107
|
+
exit: (code) => {
|
|
8108
|
+
exitCode = code;
|
|
8109
|
+
resolveFinished();
|
|
8110
|
+
},
|
|
8111
|
+
log: errorLine,
|
|
8112
|
+
every: (fn, ms) => {
|
|
8113
|
+
const t = setInterval(fn, ms);
|
|
8114
|
+
t.unref();
|
|
8115
|
+
return () => clearInterval(t);
|
|
8116
|
+
},
|
|
8117
|
+
...deps.restart
|
|
8118
|
+
});
|
|
8119
|
+
restarter.start();
|
|
8120
|
+
await finished;
|
|
8121
|
+
restarter.stop();
|
|
8122
|
+
process.off("SIGINT", onSignal);
|
|
8123
|
+
process.off("SIGTERM", onSignal);
|
|
8124
|
+
process.off("exit", onExit);
|
|
8125
|
+
return exitCode;
|
|
8126
|
+
}
|
|
8127
|
+
async function hubStart(deps = {}) {
|
|
8128
|
+
const home = deps.home ?? caddisHome();
|
|
8129
|
+
const alive = deps.alive ?? pidAlive;
|
|
8130
|
+
try {
|
|
8131
|
+
resolvePort(deps);
|
|
8132
|
+
} catch (e2) {
|
|
8133
|
+
errorLine(message2(e2));
|
|
8134
|
+
return 1;
|
|
8135
|
+
}
|
|
8136
|
+
const st = await inspect(deps);
|
|
8137
|
+
if (st.kind === "running" || st.kind === "starting" || st.kind === "unresponsive") {
|
|
8138
|
+
errorLine(refusal(st));
|
|
8139
|
+
return 1;
|
|
8140
|
+
}
|
|
8141
|
+
if (st.kind === "stale") removeLockIf(st.lock, home);
|
|
8142
|
+
let key;
|
|
8143
|
+
try {
|
|
8144
|
+
key = await ensureKey({ home, runCmd: deps.runCmd });
|
|
8145
|
+
} catch (e2) {
|
|
8146
|
+
errorLine(message2(e2));
|
|
8147
|
+
return 1;
|
|
8148
|
+
}
|
|
8149
|
+
const entry = deps.entry ?? process.argv[1];
|
|
8150
|
+
if (!entry) {
|
|
8151
|
+
errorLine("cannot find the caddis CLI entry script to start the hub");
|
|
8152
|
+
return 1;
|
|
8153
|
+
}
|
|
8154
|
+
const cwd = hubDir(home);
|
|
8155
|
+
mkdirSync7(cwd, { recursive: true });
|
|
8156
|
+
const child = await (deps.spawn ?? defaultSpawn)(process.execPath, [entry, "hub", "serve"], { cwd });
|
|
8157
|
+
let exited = false;
|
|
8158
|
+
let exitCode = null;
|
|
8159
|
+
void child.exited.then((c) => {
|
|
8160
|
+
exited = true;
|
|
8161
|
+
exitCode = c;
|
|
8162
|
+
});
|
|
8163
|
+
const waitMs = deps.startWaitMs ?? START_WAIT_MS;
|
|
8164
|
+
const deadline = Date.now() + waitMs;
|
|
8165
|
+
while (Date.now() < deadline) {
|
|
8166
|
+
if (exited) {
|
|
8167
|
+
errorLine(`caddis hub serve exited (code ${exitCode}) before it answered; run "caddis hub serve" to see the error`);
|
|
8168
|
+
return 1;
|
|
8169
|
+
}
|
|
8170
|
+
const lock = readLock(home);
|
|
8171
|
+
if (lock && lock.pid === child.pid && lock.port > 0 && lock.instance && await whoami(lock.port) === lock.instance) {
|
|
8172
|
+
const h2 = await hubRequest("GET", lock.port, "/api/health", key, 1e3);
|
|
8173
|
+
if (h2?.status === 200) {
|
|
8174
|
+
line(`caddis hub running on ${hubUrl(lock.port)}`);
|
|
8175
|
+
line("open it with: caddis hub open");
|
|
8176
|
+
return 0;
|
|
8177
|
+
}
|
|
8178
|
+
}
|
|
8179
|
+
await sleep(100);
|
|
8180
|
+
}
|
|
8181
|
+
if (child.pid && alive(child.pid)) await (deps.kill ?? killTree2)(child.pid);
|
|
8182
|
+
errorLine(
|
|
8183
|
+
`caddis hub did not answer within ${Math.round(waitMs / 1e3)} s; run "caddis hub serve" to see the error`
|
|
8184
|
+
);
|
|
8185
|
+
return 1;
|
|
8186
|
+
}
|
|
8187
|
+
async function runningHub(deps) {
|
|
8188
|
+
const home = deps.home ?? caddisHome();
|
|
8189
|
+
const st = await inspect(deps);
|
|
8190
|
+
if (st.kind !== "running") return null;
|
|
8191
|
+
const key = readKeyQuiet(home);
|
|
8192
|
+
if (!key) return null;
|
|
8193
|
+
const h2 = await hubRequest("GET", st.lock.port, "/api/health", key, HEALTH_TIMEOUT_MS);
|
|
8194
|
+
return h2?.status === 200 ? { lock: st.lock, key, body: h2.body } : null;
|
|
8195
|
+
}
|
|
8196
|
+
function formatUptime(ms) {
|
|
8197
|
+
let s = Math.max(0, Math.floor(ms / 1e3));
|
|
8198
|
+
const d3 = Math.floor(s / 86400);
|
|
8199
|
+
s -= d3 * 86400;
|
|
8200
|
+
const h2 = Math.floor(s / 3600);
|
|
8201
|
+
s -= h2 * 3600;
|
|
8202
|
+
const m2 = Math.floor(s / 60);
|
|
8203
|
+
s -= m2 * 60;
|
|
8204
|
+
const parts = [];
|
|
8205
|
+
if (d3) parts.push(`${d3}d`);
|
|
8206
|
+
if (d3 || h2) parts.push(`${h2}h`);
|
|
8207
|
+
if (d3 || h2 || m2) parts.push(`${m2}m`);
|
|
8208
|
+
parts.push(`${s}s`);
|
|
8209
|
+
return parts.join(" ");
|
|
8210
|
+
}
|
|
8211
|
+
async function hubStatus(opts = {}) {
|
|
8212
|
+
const now = opts.now ?? Date.now;
|
|
8213
|
+
const hub2 = await runningHub(opts);
|
|
8214
|
+
if (!hub2) {
|
|
8215
|
+
if (opts.json) line(JSON.stringify({ schema: 1, running: false, port: null, pid: null, versions: null }));
|
|
8216
|
+
else line("caddis hub is not running; start it with: caddis hub start");
|
|
8217
|
+
return 1;
|
|
8218
|
+
}
|
|
8219
|
+
const versions = hub2.body.versions ?? {};
|
|
8220
|
+
if (opts.json) {
|
|
8221
|
+
line(JSON.stringify({ schema: 1, running: true, port: hub2.lock.port, pid: hub2.lock.pid, versions }));
|
|
8222
|
+
return 0;
|
|
8223
|
+
}
|
|
8224
|
+
const started = Date.parse(hub2.lock.started_at);
|
|
8225
|
+
line(`caddis hub running on ${hubUrl(hub2.lock.port)}`);
|
|
8226
|
+
line(` pid ${hub2.lock.pid}, uptime ${Number.isNaN(started) ? "unknown" : formatUptime(now() - started)}`);
|
|
8227
|
+
line(` versions: ${Object.entries(versions).map(([k3, v2]) => `${k3} ${v2}`).join(", ")}`);
|
|
8228
|
+
return 0;
|
|
8229
|
+
}
|
|
8230
|
+
async function hubStop(deps = {}) {
|
|
8231
|
+
const home = deps.home ?? caddisHome();
|
|
8232
|
+
const alive = deps.alive ?? pidAlive;
|
|
8233
|
+
const waitMs = deps.stopWaitMs ?? STOP_WAIT_MS;
|
|
8234
|
+
const st = await inspect(deps);
|
|
8235
|
+
if (st.kind === "none") {
|
|
8236
|
+
line("caddis hub is not running");
|
|
8237
|
+
return 0;
|
|
8238
|
+
}
|
|
8239
|
+
const lock = st.lock;
|
|
8240
|
+
if (st.kind === "running") {
|
|
8241
|
+
const key = readKeyQuiet(home);
|
|
8242
|
+
const r2 = key ? await hubRequest("POST", lock.port, "/api/hub/stop", key, waitMs) : null;
|
|
8243
|
+
if (r2?.status === 200) {
|
|
8244
|
+
const deadline2 = Date.now() + waitMs;
|
|
8245
|
+
while (Date.now() < deadline2) {
|
|
8246
|
+
const cur = readLock(home);
|
|
8247
|
+
if (!cur || cur.instance !== lock.instance || !holderAlive(cur, home, alive)) {
|
|
8248
|
+
line("caddis hub stopped");
|
|
8249
|
+
return 0;
|
|
8250
|
+
}
|
|
8251
|
+
await sleep(50);
|
|
8252
|
+
}
|
|
8253
|
+
}
|
|
8254
|
+
}
|
|
8255
|
+
if (!holderAlive(lock, home, alive)) {
|
|
8256
|
+
removeLockIf(lock, home);
|
|
8257
|
+
line("caddis hub is not running (removed a stale lock)");
|
|
8258
|
+
return 0;
|
|
8259
|
+
}
|
|
8260
|
+
const same = await sameProcess(lock, deps);
|
|
8261
|
+
if (same === "unknown") {
|
|
8262
|
+
errorLine(`caddis hub pid ${lock.pid} does not answer and its start time cannot be read; lock kept. Retry, or end that pid by hand`);
|
|
8263
|
+
return 1;
|
|
8264
|
+
}
|
|
8265
|
+
const ours = same === "same" && await (deps.isHubPid ?? isNodeProcess)(lock.pid);
|
|
8266
|
+
if (!ours) {
|
|
8267
|
+
removeLockIf(lock, home);
|
|
8268
|
+
line(`caddis hub is not running (removed a stale lock; pid ${lock.pid} is another program, not killed)`);
|
|
8269
|
+
return 0;
|
|
8270
|
+
}
|
|
8271
|
+
await (deps.kill ?? killTree2)(lock.pid);
|
|
8272
|
+
const deadline = Date.now() + waitMs;
|
|
8273
|
+
while (alive(lock.pid) && Date.now() < deadline) await sleep(50);
|
|
8274
|
+
if (alive(lock.pid)) {
|
|
8275
|
+
errorLine(`could not stop caddis hub (pid ${lock.pid})`);
|
|
8276
|
+
return 1;
|
|
8277
|
+
}
|
|
8278
|
+
removeLockIf(lock, home);
|
|
8279
|
+
line(`caddis hub stopped (killed pid ${lock.pid})`);
|
|
8280
|
+
return 0;
|
|
8281
|
+
}
|
|
8282
|
+
async function hubOpen(deps = {}) {
|
|
8283
|
+
const now = deps.now ?? Date.now;
|
|
8284
|
+
const hub2 = await runningHub(deps);
|
|
8285
|
+
if (!hub2) {
|
|
8286
|
+
errorLine("caddis hub is not running; start it with: caddis hub start");
|
|
8287
|
+
return 1;
|
|
8288
|
+
}
|
|
8289
|
+
const r2 = await hubRequest("POST", hub2.lock.port, "/api/hub/code", hub2.key, HEALTH_TIMEOUT_MS);
|
|
8290
|
+
const code = r2?.status === 200 ? r2.body.code : void 0;
|
|
8291
|
+
if (typeof code !== "string" || !CODE_RE.test(code)) {
|
|
8292
|
+
errorLine(`caddis hub did not give a valid login code (HTTP ${r2?.status ?? "no answer"})`);
|
|
8293
|
+
return 1;
|
|
8294
|
+
}
|
|
8295
|
+
const url = `${hubUrl(hub2.lock.port)}login?code=${code}`;
|
|
8296
|
+
const expires = Date.parse(String(r2?.body.expires_at ?? ""));
|
|
8297
|
+
const secs = Number.isNaN(expires) ? 60 : Math.round((expires - now()) / 1e3);
|
|
8298
|
+
line(url);
|
|
8299
|
+
line(`the code works once, for ${secs} s`);
|
|
8300
|
+
let opened = false;
|
|
8301
|
+
try {
|
|
8302
|
+
opened = await (deps.opener ?? defaultOpener)(url);
|
|
8303
|
+
} catch {
|
|
8304
|
+
opened = false;
|
|
8305
|
+
}
|
|
8306
|
+
if (!opened) line("no browser opened: open this URL in your browser");
|
|
8307
|
+
return 0;
|
|
8308
|
+
}
|
|
8309
|
+
async function hubAutostart(action, deps = {}) {
|
|
8310
|
+
try {
|
|
8311
|
+
const r2 = action === "enable" ? await enableAutostart(deps) : action === "disable" ? await disableAutostart(deps) : action === "status" ? await autostartStatus(deps) : null;
|
|
8312
|
+
if (!r2) {
|
|
8313
|
+
errorLine(`unknown action "${action}": use enable, disable or status`);
|
|
8314
|
+
return 1;
|
|
8315
|
+
}
|
|
8316
|
+
(r2.ok || action === "status" ? line : errorLine)(r2.message);
|
|
8317
|
+
return r2.ok ? 0 : 1;
|
|
8318
|
+
} catch (e2) {
|
|
8319
|
+
errorLine(message2(e2));
|
|
8320
|
+
return 1;
|
|
8321
|
+
}
|
|
8322
|
+
}
|
|
8323
|
+
var SPAWN_OPTIONS, START_WAIT_MS, WHOAMI_TIMEOUT_MS, CODE_RE, STOP_WAIT_MS, HEALTH_TIMEOUT_MS, TOOL_TIMEOUT_MS, HubError, sleep, hubUrl, message2, alreadyRunning, unresponsive, refusal;
|
|
8324
|
+
var init_hub = __esm({
|
|
8325
|
+
"src/commands/hub.ts"() {
|
|
8326
|
+
"use strict";
|
|
8327
|
+
init_autostart();
|
|
8328
|
+
init_config();
|
|
8329
|
+
init_keyfile();
|
|
8330
|
+
init_lock();
|
|
8331
|
+
init_paths();
|
|
8332
|
+
init_restart();
|
|
8333
|
+
init_server();
|
|
8334
|
+
init_state();
|
|
8335
|
+
init_exec();
|
|
8336
|
+
init_log();
|
|
8337
|
+
init_pkg();
|
|
8338
|
+
init_sessions();
|
|
8339
|
+
SPAWN_OPTIONS = {
|
|
8340
|
+
detached: true,
|
|
8341
|
+
stdio: "ignore",
|
|
8342
|
+
windowsHide: true,
|
|
8343
|
+
shell: false,
|
|
8344
|
+
cleanup: false,
|
|
8345
|
+
reject: false
|
|
8346
|
+
};
|
|
8347
|
+
START_WAIT_MS = 2e4;
|
|
8348
|
+
WHOAMI_TIMEOUT_MS = 1500;
|
|
8349
|
+
CODE_RE = /^[A-Za-z0-9_-]+$/;
|
|
8350
|
+
STOP_WAIT_MS = 5e3;
|
|
8351
|
+
HEALTH_TIMEOUT_MS = 3e3;
|
|
8352
|
+
TOOL_TIMEOUT_MS = 15e3;
|
|
8353
|
+
HubError = class extends Error {
|
|
8354
|
+
};
|
|
8355
|
+
sleep = (ms) => new Promise((r2) => setTimeout(r2, ms));
|
|
8356
|
+
hubUrl = (port) => `http://127.0.0.1:${port}/`;
|
|
8357
|
+
message2 = (e2) => e2 instanceof Error ? e2.message : String(e2);
|
|
8358
|
+
alreadyRunning = (l2) => `caddis hub is already running (pid ${l2.pid}, port ${l2.port})`;
|
|
8359
|
+
unresponsive = (l2) => `caddis hub pid ${l2.pid} holds the lock but does not answer: run caddis hub stop`;
|
|
8360
|
+
refusal = (st) => st.kind === "unresponsive" ? unresponsive(st.lock) : alreadyRunning(st.lock);
|
|
8361
|
+
}
|
|
8362
|
+
});
|
|
8363
|
+
|
|
8364
|
+
// node_modules/commander/esm.mjs
|
|
8365
|
+
var import_index = __toESM(require_commander(), 1);
|
|
8366
|
+
var {
|
|
8367
|
+
program,
|
|
8368
|
+
createCommand,
|
|
8369
|
+
createArgument,
|
|
8370
|
+
createOption,
|
|
8371
|
+
CommanderError,
|
|
8372
|
+
InvalidArgumentError,
|
|
8373
|
+
InvalidOptionArgumentError,
|
|
8374
|
+
// deprecated old name
|
|
8375
|
+
Command,
|
|
8376
|
+
Argument,
|
|
8377
|
+
Option,
|
|
8378
|
+
Help
|
|
8379
|
+
} = import_index.default;
|
|
8380
|
+
|
|
8381
|
+
// src/agents/claude.ts
|
|
8382
|
+
init_exec();
|
|
8383
|
+
init_which();
|
|
8384
|
+
var BIN = "claude";
|
|
8385
|
+
var MARKETPLACE = "caddis";
|
|
8386
|
+
var PLUGIN = "caddis@caddis";
|
|
8387
|
+
function parsePluginList(stdout, plugin = PLUGIN) {
|
|
8388
|
+
const wanted = plugin.split("@")[0] ?? plugin;
|
|
8389
|
+
for (const [name, body] of pluginBlocks(stdout)) {
|
|
8390
|
+
if (name !== wanted) continue;
|
|
8391
|
+
const entry = {};
|
|
8392
|
+
for (const line2 of body) {
|
|
8393
|
+
const version = line2.match(/^\s*Version\s*:\s*(.+?)\s*$/i);
|
|
8394
|
+
if (version?.[1] && entry.version === void 0) {
|
|
8395
|
+
entry.version = version[1] === "unknown" ? void 0 : version[1];
|
|
8396
|
+
}
|
|
8397
|
+
const status5 = line2.match(/^\s*Status\s*:\s*(.+?)\s*$/i);
|
|
8398
|
+
if (status5?.[1]) entry.disabled = /disabled/i.test(status5[1]);
|
|
8399
|
+
}
|
|
8400
|
+
return entry;
|
|
8401
|
+
}
|
|
8402
|
+
return null;
|
|
8403
|
+
}
|
|
8404
|
+
function* pluginBlocks(stdout) {
|
|
8405
|
+
const HEADER = /^\s*(?:\S\s+)?([A-Za-z0-9._-]+)@([A-Za-z0-9._-]+)\s*$/;
|
|
8406
|
+
let current = null;
|
|
8407
|
+
for (const line2 of stdout.split(/\r?\n/)) {
|
|
8408
|
+
const header = line2.match(HEADER);
|
|
8409
|
+
if (header?.[1]) {
|
|
8410
|
+
if (current) yield current;
|
|
8411
|
+
current = [header[1], []];
|
|
8412
|
+
} else if (current) {
|
|
8413
|
+
current[1].push(line2);
|
|
8414
|
+
}
|
|
8415
|
+
}
|
|
8416
|
+
if (current) yield current;
|
|
8417
|
+
}
|
|
8418
|
+
async function detect() {
|
|
8419
|
+
const path20 = await findBin(BIN);
|
|
8420
|
+
if (!path20) {
|
|
8421
|
+
return { present: false, note: "no `claude` binary on PATH" };
|
|
8422
|
+
}
|
|
8423
|
+
const version = await run(BIN, ["--version"], { timeout: 2e4 });
|
|
8424
|
+
return {
|
|
8425
|
+
present: true,
|
|
8426
|
+
path: path20,
|
|
8427
|
+
agentVersion: version.ok ? version.stdout.trim().split(/\s+/)[0] || void 0 : void 0,
|
|
8428
|
+
note: version.ok ? void 0 : "binary found but `claude --version` failed"
|
|
8429
|
+
};
|
|
8430
|
+
}
|
|
8431
|
+
async function status() {
|
|
8432
|
+
const detected = await detect();
|
|
8433
|
+
if (!detected.present) return { installed: false, note: "agent not installed" };
|
|
8434
|
+
const listed = await run(BIN, ["plugin", "list"], { timeout: 6e4 });
|
|
8435
|
+
if (!listed.ok) {
|
|
8436
|
+
return {
|
|
8437
|
+
installed: false,
|
|
8438
|
+
source: "`claude plugin list`",
|
|
8439
|
+
note: `could not read plugin list (${listed.failure ?? `exit ${listed.code}`})`
|
|
8440
|
+
};
|
|
8441
|
+
}
|
|
8442
|
+
const entry = parsePluginList(listed.stdout);
|
|
8443
|
+
if (!entry) {
|
|
8444
|
+
return { installed: false, source: "`claude plugin list`", note: "caddis plugin not installed" };
|
|
8445
|
+
}
|
|
8446
|
+
return {
|
|
8447
|
+
installed: true,
|
|
8448
|
+
version: entry.version,
|
|
8449
|
+
disabled: entry.disabled,
|
|
8450
|
+
source: "`claude plugin list`",
|
|
8451
|
+
note: entry.version ? void 0 : "installed but the version is not reported"
|
|
8452
|
+
};
|
|
8453
|
+
}
|
|
8454
|
+
function steps(action) {
|
|
8455
|
+
if (action === "install") {
|
|
8456
|
+
return [
|
|
8457
|
+
{ cmd: BIN, args: ["plugin", "marketplace", "add", "saajunaid/caddis-plugin"] },
|
|
8458
|
+
{ cmd: BIN, args: ["plugin", "marketplace", "update", MARKETPLACE] },
|
|
8459
|
+
{ cmd: BIN, args: ["plugin", "install", PLUGIN] }
|
|
8460
|
+
];
|
|
8461
|
+
}
|
|
8462
|
+
return [
|
|
8463
|
+
{ cmd: BIN, args: ["plugin", "marketplace", "update", MARKETPLACE] },
|
|
8464
|
+
{ cmd: BIN, args: ["plugin", "update", PLUGIN] }
|
|
8465
|
+
];
|
|
8466
|
+
}
|
|
8467
|
+
async function drive(action, options) {
|
|
8468
|
+
const detected = await detect();
|
|
8469
|
+
if (!detected.present) {
|
|
8470
|
+
return { ok: true, skipped: true, steps: [], message: "Claude Code not installed \u2014 skipped" };
|
|
8471
|
+
}
|
|
8472
|
+
const planned = steps(action);
|
|
8473
|
+
if (options.dryRun) {
|
|
8474
|
+
return {
|
|
8475
|
+
ok: true,
|
|
8476
|
+
skipped: true,
|
|
8477
|
+
steps: planned.map((step) => ({ command: formatCommand(step.cmd, step.args), ok: true, code: null })),
|
|
8478
|
+
message: "dry run \u2014 nothing executed"
|
|
8479
|
+
};
|
|
8480
|
+
}
|
|
8481
|
+
const results = [];
|
|
8482
|
+
for (const step of planned) {
|
|
8483
|
+
const command = formatCommand(step.cmd, step.args);
|
|
8484
|
+
const result = await run(step.cmd, step.args);
|
|
8485
|
+
const isMarketplaceAdd = step.args[1] === "marketplace" && step.args[2] === "add";
|
|
8486
|
+
const isMarketplaceUpdate = step.args[1] === "marketplace" && step.args[2] === "update";
|
|
8487
|
+
let ok = result.ok;
|
|
8488
|
+
let code = result.code;
|
|
8489
|
+
if (!ok && isMarketplaceAdd) {
|
|
8490
|
+
const output = `${result.stdout}
|
|
8491
|
+
${result.stderr}`;
|
|
8492
|
+
if (/already (exists|added|configured|installed|on disk)/i.test(output)) {
|
|
8493
|
+
ok = true;
|
|
8494
|
+
code = 0;
|
|
8495
|
+
}
|
|
8496
|
+
}
|
|
8497
|
+
results.push({
|
|
8498
|
+
command,
|
|
8499
|
+
ok,
|
|
8500
|
+
code,
|
|
8501
|
+
output: ok ? void 0 : tail(result.stderr || result.stdout || result.failure || "")
|
|
8502
|
+
});
|
|
8503
|
+
if (!ok && !isMarketplaceUpdate) {
|
|
8504
|
+
return { ok: false, skipped: false, steps: results, message: `\`${command}\` failed` };
|
|
8505
|
+
}
|
|
8506
|
+
}
|
|
8507
|
+
return { ok: true, skipped: false, steps: results };
|
|
8508
|
+
}
|
|
8509
|
+
function tail(text, lines = 3) {
|
|
8510
|
+
return text.trim().split(/\r?\n/).slice(-lines).join("\n");
|
|
8511
|
+
}
|
|
8512
|
+
var claudeAdapter = {
|
|
8513
|
+
id: "claude",
|
|
8514
|
+
name: "Claude Code",
|
|
8515
|
+
supported: true,
|
|
8516
|
+
summary: "refresh the caddis marketplace, then update the caddis plugin",
|
|
8517
|
+
detect,
|
|
8518
|
+
drive,
|
|
8519
|
+
status
|
|
8520
|
+
};
|
|
8521
|
+
|
|
8522
|
+
// src/agents/agy.ts
|
|
8523
|
+
init_exec();
|
|
8524
|
+
init_which();
|
|
8525
|
+
import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, mkdtempSync, mkdirSync, cpSync, rmSync, renameSync } from "fs";
|
|
8526
|
+
import os from "os";
|
|
8527
|
+
import path2 from "path";
|
|
8528
|
+
|
|
8529
|
+
// src/util/agyProcs.ts
|
|
8530
|
+
init_exec();
|
|
8531
|
+
function parseTasklistCsv(stdout) {
|
|
8532
|
+
const pids = [];
|
|
8533
|
+
for (const line2 of stdout.split(/\r?\n/)) {
|
|
8534
|
+
const match = /^"agy\.exe","(\d+)"/i.exec(line2.trim());
|
|
8535
|
+
if (match) pids.push(Number(match[1]));
|
|
8536
|
+
}
|
|
8537
|
+
return pids;
|
|
8538
|
+
}
|
|
8539
|
+
function parsePgrep(stdout) {
|
|
8540
|
+
return stdout.split(/\r?\n/).map((line2) => line2.trim()).filter((line2) => /^\d+$/.test(line2)).map(Number);
|
|
8541
|
+
}
|
|
8542
|
+
async function listAgyPids() {
|
|
8543
|
+
if (process.platform === "win32") {
|
|
8544
|
+
const result2 = await run("tasklist", ["/FI", "IMAGENAME eq agy.exe", "/FO", "CSV", "/NH"], { timeout: 2e4 });
|
|
8545
|
+
return result2.ok ? parseTasklistCsv(result2.stdout) : [];
|
|
8546
|
+
}
|
|
8547
|
+
const result = await run("pgrep", ["-x", "agy"], { timeout: 1e4 });
|
|
8548
|
+
return result.ok ? parsePgrep(result.stdout) : [];
|
|
8549
|
+
}
|
|
8550
|
+
|
|
8551
|
+
// src/agents/agy.ts
|
|
8552
|
+
init_pkg();
|
|
8553
|
+
var BIN2 = "agy";
|
|
8554
|
+
var BUNDLE = "antigravity-plugin";
|
|
8555
|
+
var PLUGIN2 = "caddis";
|
|
8556
|
+
var EXTRAS_BUNDLE = "antigravity-plugin-extras";
|
|
8557
|
+
var EXTRAS_PLUGIN = "caddis-extras";
|
|
8558
|
+
var REQUIRED_PLUGIN_FILES = ["plugin.json", "hooks.json", "guard_agy.py"];
|
|
8559
|
+
var ALLOW_RUNNING_ENV = "CADDIS_AGY_ALLOW_RUNNING";
|
|
8560
|
+
function pluginDirFor(home = os.homedir(), plugin = PLUGIN2) {
|
|
8561
|
+
return path2.join(home, ".gemini", "config", "plugins", plugin);
|
|
8562
|
+
}
|
|
8563
|
+
function missingPluginFiles(home = os.homedir()) {
|
|
8564
|
+
const dir = pluginDirFor(home);
|
|
8565
|
+
return REQUIRED_PLUGIN_FILES.filter((file) => !existsSync2(path2.join(dir, file)));
|
|
5912
8566
|
}
|
|
5913
8567
|
function backupInstalledPlugin(home) {
|
|
5914
8568
|
const dir = pluginDirFor(home);
|
|
@@ -5946,8 +8600,8 @@ function errorText(error) {
|
|
|
5946
8600
|
if (!(error instanceof Error)) return String(error);
|
|
5947
8601
|
const code = error.code;
|
|
5948
8602
|
const kind = code ?? error.constructor.name;
|
|
5949
|
-
const
|
|
5950
|
-
return `${kind}: ${
|
|
8603
|
+
const message3 = code && error.message.startsWith(`${code}: `) ? error.message.slice(code.length + 2) : error.message;
|
|
8604
|
+
return `${kind}: ${message3}`;
|
|
5951
8605
|
}
|
|
5952
8606
|
function stampMs(stamp) {
|
|
5953
8607
|
const n = Number(stamp);
|
|
@@ -6212,8 +8866,8 @@ async function drive2(_action, options) {
|
|
|
6212
8866
|
if (missing.length > 0) {
|
|
6213
8867
|
let outcome = "there was nothing to restore";
|
|
6214
8868
|
if (backup) {
|
|
6215
|
-
const
|
|
6216
|
-
outcome =
|
|
8869
|
+
const failure2 = backup.restore();
|
|
8870
|
+
outcome = failure2 === null ? "the previous plugin was restored" : `the restore FAILED (${failure2}); the old plugin is kept at ${backup.location}`;
|
|
6217
8871
|
}
|
|
6218
8872
|
steps3.push({ command, ok: false, code: result.code, output: `missing after the install: ${missing.join(", ")}` });
|
|
6219
8873
|
return {
|
|
@@ -6430,8 +9084,8 @@ ${result.stderr}`;
|
|
|
6430
9084
|
rmSync2(legacyDir, { recursive: true, force: true });
|
|
6431
9085
|
results.push({ command: removalCommand, ok: true, code: 0 });
|
|
6432
9086
|
} catch (error) {
|
|
6433
|
-
const
|
|
6434
|
-
results.push({ command: removalCommand, ok: false, code: null, output:
|
|
9087
|
+
const message3 = error instanceof Error ? error.message : String(error);
|
|
9088
|
+
results.push({ command: removalCommand, ok: false, code: null, output: message3 });
|
|
6435
9089
|
}
|
|
6436
9090
|
}
|
|
6437
9091
|
const notes = [];
|
|
@@ -6476,8 +9130,8 @@ function createStubAdapter(spec) {
|
|
|
6476
9130
|
const found = await findBin(bin);
|
|
6477
9131
|
if (found) return { present: true, path: found, note: NOT_YET_SUPPORTED };
|
|
6478
9132
|
}
|
|
6479
|
-
for (const
|
|
6480
|
-
if (existsSync4(
|
|
9133
|
+
for (const configPath2 of spec.configPaths) {
|
|
9134
|
+
if (existsSync4(configPath2)) return { present: true, path: configPath2, note: NOT_YET_SUPPORTED };
|
|
6481
9135
|
}
|
|
6482
9136
|
return { present: false, note: `no ${spec.bins.map((b3) => `\`${b3}\``).join(" / ")} binary or config found` };
|
|
6483
9137
|
}
|
|
@@ -6594,10 +9248,35 @@ withGlobalFlags(program2.command("status")).description("compact table: cli vers
|
|
|
6594
9248
|
const { status: status5 } = await Promise.resolve().then(() => (init_status(), status_exports));
|
|
6595
9249
|
finish(await status5({ adapters: resolveAdapters(flags), json: options.json }));
|
|
6596
9250
|
});
|
|
9251
|
+
var hub = program2.command("hub").description("the local caddis hub web server (127.0.0.1 only)");
|
|
9252
|
+
hub.command("start").description("start the hub in the background (hidden)").action(async () => {
|
|
9253
|
+
const { hubStart: hubStart2 } = await Promise.resolve().then(() => (init_hub(), hub_exports));
|
|
9254
|
+
finish(await hubStart2());
|
|
9255
|
+
});
|
|
9256
|
+
hub.command("stop").description("stop the running hub").action(async () => {
|
|
9257
|
+
const { hubStop: hubStop2 } = await Promise.resolve().then(() => (init_hub(), hub_exports));
|
|
9258
|
+
finish(await hubStop2());
|
|
9259
|
+
});
|
|
9260
|
+
hub.command("status").description("is the hub running: port, pid, versions, uptime (exit 1 when down)").option("--json", "machine-readable output", false).action(async (options) => {
|
|
9261
|
+
const { hubStatus: hubStatus2 } = await Promise.resolve().then(() => (init_hub(), hub_exports));
|
|
9262
|
+
finish(await hubStatus2({ json: options.json === true }));
|
|
9263
|
+
});
|
|
9264
|
+
hub.command("open").description("open the hub in your browser with a one-time login code").action(async () => {
|
|
9265
|
+
const { hubOpen: hubOpen2 } = await Promise.resolve().then(() => (init_hub(), hub_exports));
|
|
9266
|
+
finish(await hubOpen2());
|
|
9267
|
+
});
|
|
9268
|
+
hub.command("serve").description("run the hub in the foreground (used by start and the logon task)").action(async () => {
|
|
9269
|
+
const { hubServe: hubServe2 } = await Promise.resolve().then(() => (init_hub(), hub_exports));
|
|
9270
|
+
finish(await hubServe2());
|
|
9271
|
+
});
|
|
9272
|
+
hub.command("autostart <action>").description("start the hub at logon: enable, disable or status (per user, opt-in)").action(async (action) => {
|
|
9273
|
+
const { hubAutostart: hubAutostart2 } = await Promise.resolve().then(() => (init_hub(), hub_exports));
|
|
9274
|
+
finish(await hubAutostart2(action));
|
|
9275
|
+
});
|
|
6597
9276
|
program2.addHelpText(
|
|
6598
9277
|
"after",
|
|
6599
9278
|
`
|
|
6600
|
-
${
|
|
9279
|
+
${color2.bold("Examples")}
|
|
6601
9280
|
$ npx @caddis/cli init detect your agents and install caddis into each
|
|
6602
9281
|
$ caddis doctor what is installed where, and what to do about it
|
|
6603
9282
|
$ caddis update --dry-run preview the exact vendor commands
|
|
@@ -6605,7 +9284,7 @@ ${color.bold("Examples")}
|
|
|
6605
9284
|
$ caddis init --extras add the optional caddis-extras skill library
|
|
6606
9285
|
$ caddis doctor --strict exit non-zero on drift (CI)
|
|
6607
9286
|
|
|
6608
|
-
${
|
|
9287
|
+
${color2.bold("Supported agents")}
|
|
6609
9288
|
claude Claude Code via its plugin marketplace
|
|
6610
9289
|
agy Antigravity via the caddis bundle shipped in this package
|
|
6611
9290
|
(--extras adds caddis-extras; already-installed
|