testdriverai 4.1.57 → 4.2.0-test.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test_interp.yml +27 -0
- package/.github/workflows/trigger_testdriver_dev.yml +4 -4
- package/agent.js +21 -24
- package/chrome.ps1 +33 -0
- package/lib/cli.js +22 -0
- package/lib/commands.js +4 -1
- package/lib/generator.js +4 -10
- package/lib/sdk.js +1 -1
- package/lib/session.js +1 -3
- package/lib/system.js +4 -23
- package/package.json +7 -9
- package/shot.png +0 -0
- package/test.js +31 -0
- package/test.yml +18 -0
- package/.github/dependabot.yml +0 -11
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
name: Example Todo App
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: ["main"]
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
run:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: testdriverai/action@alexrowe/yml-interpolation-test
|
|
15
|
+
with:
|
|
16
|
+
os: windows
|
|
17
|
+
branch: alexrowe/yml-interpolation
|
|
18
|
+
key: ${{secrets.TESTDRIVER_API_KEY}}
|
|
19
|
+
prompt: |
|
|
20
|
+
1. /run test.yml
|
|
21
|
+
prerun: |
|
|
22
|
+
Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "${{ env.WEBSITE_URL }}"
|
|
23
|
+
|
|
24
|
+
env:
|
|
25
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
26
|
+
WEBSITE_URL: "https://kzmgtp9k2gtuuw2iquk2.lite.vusercontent.net/"
|
|
27
|
+
TD_PASSWORD: "passW0rd!"
|
package/agent.js
CHANGED
|
@@ -202,11 +202,8 @@ if (!commandHistory.length) {
|
|
|
202
202
|
];
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
const exit = async (failed = true
|
|
206
|
-
|
|
207
|
-
if (shouldSave) {
|
|
208
|
-
await save();
|
|
209
|
-
}
|
|
205
|
+
const exit = async (failed = true) => {
|
|
206
|
+
await save();
|
|
210
207
|
|
|
211
208
|
analytics.track("exit", { failed });
|
|
212
209
|
|
|
@@ -644,16 +641,6 @@ const actOnMarkdown = async (content, depth, pushToHistory = false) => {
|
|
|
644
641
|
// simple function to backfill the chat history with a prompt and
|
|
645
642
|
// then call `promptUser()` to get the user input
|
|
646
643
|
const firstPrompt = async () => {
|
|
647
|
-
|
|
648
|
-
// should be start of new session
|
|
649
|
-
const sessionRes = await sdk.req("session/start", {
|
|
650
|
-
systemInformationOsInfo: await system.getSystemInformationOsInfo(),
|
|
651
|
-
mousePosition: await system.getMousePosition(),
|
|
652
|
-
activeWindow: await system.activeWin(),
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
session.set(sessionRes.data.id);
|
|
656
|
-
|
|
657
644
|
// readline is what allows us to get user input
|
|
658
645
|
rl = readline.createInterface({
|
|
659
646
|
terminal: true,
|
|
@@ -694,7 +681,7 @@ const firstPrompt = async () => {
|
|
|
694
681
|
if (input.indexOf("/summarize") == 0) {
|
|
695
682
|
await summarize();
|
|
696
683
|
} else if (input.indexOf("/quit") == 0) {
|
|
697
|
-
await exit(
|
|
684
|
+
await exit();
|
|
698
685
|
} else if (input.indexOf("/save") == 0) {
|
|
699
686
|
await save({ filepath: commands[1] });
|
|
700
687
|
} else if (input.indexOf("/undo") == 0) {
|
|
@@ -704,7 +691,7 @@ const firstPrompt = async () => {
|
|
|
704
691
|
} else if (input.indexOf("/manual") == 0) {
|
|
705
692
|
await manualInput(commands.slice(1).join(" "));
|
|
706
693
|
} else if (input.indexOf("/run") == 0) {
|
|
707
|
-
await run(commands[1], commands[2]
|
|
694
|
+
await run(commands[1], commands[2], commands[3]);
|
|
708
695
|
} else if (input.indexOf("/generate") == 0) {
|
|
709
696
|
await generate(commands[1], commands[2]);
|
|
710
697
|
} else {
|
|
@@ -718,7 +705,7 @@ const firstPrompt = async () => {
|
|
|
718
705
|
// if file exists, load it
|
|
719
706
|
if (fs.existsSync(thisFile)) {
|
|
720
707
|
analytics.track("load");
|
|
721
|
-
let object = await generator.
|
|
708
|
+
let object = await generator.ymlToHistory(
|
|
722
709
|
fs.readFileSync(thisFile, "utf-8"),
|
|
723
710
|
);
|
|
724
711
|
|
|
@@ -848,7 +835,7 @@ let save = async ({ filepath = thisFile, silent = false } = {}) => {
|
|
|
848
835
|
}
|
|
849
836
|
|
|
850
837
|
// write reply to /tmp/oiResult.log.log
|
|
851
|
-
let regression = await generator.
|
|
838
|
+
let regression = await generator.historyToYml(executionHistory);
|
|
852
839
|
try {
|
|
853
840
|
fs.writeFileSync(filepath, regression);
|
|
854
841
|
} catch (e) {
|
|
@@ -875,10 +862,9 @@ ${regression}
|
|
|
875
862
|
// this will load a regression test from a file location
|
|
876
863
|
// it parses the markdown file and executes the codeblocks exactly as if they were
|
|
877
864
|
// generated by the AI in a single prompt
|
|
878
|
-
let run = async (file,
|
|
865
|
+
let run = async (file, overwrite = false, shouldExit = true) => {
|
|
879
866
|
|
|
880
867
|
setTerminalWindowTransparency(true);
|
|
881
|
-
emitter.emit(events.interactive, false);
|
|
882
868
|
|
|
883
869
|
log.log("info", chalk.cyan(`running ${file}...`));
|
|
884
870
|
|
|
@@ -898,9 +884,12 @@ let run = async (file, shouldSave = false, shouldExit = true) => {
|
|
|
898
884
|
await exit(true);
|
|
899
885
|
}
|
|
900
886
|
|
|
887
|
+
console.log(yml)
|
|
888
|
+
console.log(process.env)
|
|
901
889
|
// Inject environment variables into any ${VAR} strings
|
|
902
890
|
yml = parser.interpolate(yml, process.env);
|
|
903
891
|
|
|
892
|
+
console.log(yml)
|
|
904
893
|
let ymlObj = null;
|
|
905
894
|
try {
|
|
906
895
|
ymlObj = await yaml.load(yml);
|
|
@@ -948,14 +937,13 @@ ${yaml.dump(step)}
|
|
|
948
937
|
await actOnMarkdown(markdown, 0, true);
|
|
949
938
|
}
|
|
950
939
|
|
|
951
|
-
if (
|
|
940
|
+
if (overwrite || overwrite == "true") {
|
|
952
941
|
await save({ filepath: file });
|
|
953
942
|
}
|
|
954
943
|
|
|
955
944
|
setTerminalWindowTransparency(false);
|
|
956
|
-
emitter.emit(events.interactive, true);
|
|
957
945
|
|
|
958
|
-
if (shouldExit) {
|
|
946
|
+
if (shouldExit || shouldExit == "true") {
|
|
959
947
|
await summarize();
|
|
960
948
|
await exit(false);
|
|
961
949
|
}
|
|
@@ -1071,6 +1059,15 @@ const start = async () => {
|
|
|
1071
1059
|
console.log("");
|
|
1072
1060
|
}
|
|
1073
1061
|
|
|
1062
|
+
// should be start of new session
|
|
1063
|
+
const sessionRes = await sdk.req("session/start", {
|
|
1064
|
+
systemInformationOsInfo: await system.getSystemInformationOsInfo(),
|
|
1065
|
+
mousePosition: await system.getMousePosition(),
|
|
1066
|
+
activeWindow: await system.activeWin(),
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
session.set(sessionRes.data);
|
|
1070
|
+
|
|
1074
1071
|
analytics.track("command", { command: thisCommand, file: thisFile });
|
|
1075
1072
|
|
|
1076
1073
|
if (thisCommand == "edit") {
|
package/chrome.ps1
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Launches Google Chrome and navigates to a specified URL.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
This PowerShell script will check for the existence of the Google Chrome
|
|
7
|
+
browser at a predefined location, and if found, will open it and navigate to
|
|
8
|
+
the URL provided as an argument to the script.
|
|
9
|
+
|
|
10
|
+
.PARAMETER url
|
|
11
|
+
The URL that Google Chrome will navigate to upon launch.
|
|
12
|
+
|
|
13
|
+
.EXAMPLE
|
|
14
|
+
.\chrome.ps1 -url "http://www.example.com"
|
|
15
|
+
This example will open Google Chrome and navigate to http://www.example.com.
|
|
16
|
+
|
|
17
|
+
.NOTES
|
|
18
|
+
Google Chrome must be installed in the default location for this script
|
|
19
|
+
to function correctly.
|
|
20
|
+
|
|
21
|
+
#>
|
|
22
|
+
|
|
23
|
+
param(
|
|
24
|
+
[string]$url
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
$chromePath = "C:\Program Files\Google\Chrome\Application\chrome.exe"
|
|
28
|
+
|
|
29
|
+
if (Test-Path $chromePath) {
|
|
30
|
+
Start-Process $chromePath $url
|
|
31
|
+
} else {
|
|
32
|
+
Write-Error "Chrome is not installed at the expected location: $chromePath"
|
|
33
|
+
}
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { program } = require("commander");
|
|
2
|
+
|
|
3
|
+
program.description("");
|
|
4
|
+
program
|
|
5
|
+
.argument("[command]", "Command to run")
|
|
6
|
+
.option(
|
|
7
|
+
"-c, --cwd <string>",
|
|
8
|
+
"working directory to run in"
|
|
9
|
+
)
|
|
10
|
+
.option(
|
|
11
|
+
"-i, --id <string>",
|
|
12
|
+
"ID of server to connect to"
|
|
13
|
+
)
|
|
14
|
+
.option(
|
|
15
|
+
"-o, --output-file <string>",
|
|
16
|
+
"Output file to write the output of the command"
|
|
17
|
+
)
|
|
18
|
+
.option(
|
|
19
|
+
"-e, --env <string>",
|
|
20
|
+
"Extra environment variables"
|
|
21
|
+
)
|
|
22
|
+
.parse();
|
package/lib/commands.js
CHANGED
|
@@ -160,12 +160,15 @@ const assert = async (assertion, shouldThrow = false, async = false) => {
|
|
|
160
160
|
return handleAssertResponse(response.data);
|
|
161
161
|
}
|
|
162
162
|
};
|
|
163
|
-
const scroll = async (direction = "down", amount = 300, method = "
|
|
163
|
+
const scroll = async (direction = "down", amount = 300, method = "keyboard") => {
|
|
164
164
|
await redraw.start();
|
|
165
165
|
|
|
166
166
|
amount = parseInt(amount);
|
|
167
167
|
|
|
168
168
|
if (method === "mouse") {
|
|
169
|
+
// after experimenting, 200 is a good default for mouse, mostly as mouse will be called only when the user asks for it
|
|
170
|
+
// and that happens when keyboard scrolling cannot do things when there's a pop up that needs to be scrolled over and
|
|
171
|
+
// pop ups are usually smaller and needs a smaller amount of scrolling
|
|
169
172
|
amount = 200;
|
|
170
173
|
}
|
|
171
174
|
|
package/lib/generator.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
const yaml = require("js-yaml");
|
|
3
3
|
const chalk = require("chalk");
|
|
4
4
|
const package = require("../package.json");
|
|
5
|
-
const session = require("./session");
|
|
6
5
|
// do the actual parsing
|
|
7
6
|
// this library is very strict
|
|
8
7
|
// note that errors are sent to the AI will it may self-heal
|
|
@@ -53,30 +52,25 @@ const jsonToManual = function (json, colors = true) {
|
|
|
53
52
|
return params;
|
|
54
53
|
};
|
|
55
54
|
|
|
56
|
-
const
|
|
57
|
-
|
|
55
|
+
const historyToYml = async function (inputArray) {
|
|
58
56
|
// use yml dump to convert json to yml
|
|
59
57
|
let yml = await yaml.dump({
|
|
60
58
|
version: package.version,
|
|
61
|
-
session: session.get(),
|
|
62
59
|
steps: inputArray,
|
|
63
60
|
});
|
|
64
61
|
|
|
65
62
|
return yml;
|
|
66
63
|
};
|
|
67
64
|
|
|
68
|
-
const
|
|
65
|
+
const ymlToHistory = async function (yml) {
|
|
69
66
|
// use yml load to convert yml to json
|
|
70
67
|
let json = await yaml.load(yml);
|
|
71
|
-
|
|
72
|
-
session.set(json.session);
|
|
73
|
-
|
|
74
68
|
return json;
|
|
75
69
|
};
|
|
76
70
|
|
|
77
71
|
module.exports = {
|
|
78
72
|
manualToYml,
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
historyToYml,
|
|
74
|
+
ymlToHistory,
|
|
81
75
|
jsonToManual,
|
|
82
76
|
};
|
package/lib/sdk.js
CHANGED
|
@@ -106,7 +106,7 @@ const req = async (path, data, onChunk) => {
|
|
|
106
106
|
responseType: typeof onChunk === "function" ? "stream" : "json",
|
|
107
107
|
data: {
|
|
108
108
|
...data,
|
|
109
|
-
session: session.get(),
|
|
109
|
+
session: session.get()?.id,
|
|
110
110
|
stream: typeof onChunk === "function",
|
|
111
111
|
},
|
|
112
112
|
};
|
package/lib/session.js
CHANGED
package/lib/system.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const os = require("os");
|
|
4
4
|
const path = require("path");
|
|
5
|
+
const screenshot = require("screenshot-desktop");
|
|
5
6
|
const si = require("systeminformation");
|
|
7
|
+
const activeWindow = require("active-win");
|
|
6
8
|
const robot = require("robotjs");
|
|
7
9
|
const sharp = require("sharp");
|
|
8
10
|
const { emitter, events } = require("./events.js");
|
|
9
|
-
const { Monitor } = require("node-screenshots");
|
|
10
11
|
|
|
11
12
|
let primaryDisplay = null;
|
|
12
13
|
|
|
@@ -47,11 +48,7 @@ const captureAndResize = async (scale = 1, silent = false, mouse = false) => {
|
|
|
47
48
|
let step1 = tmpFilename();
|
|
48
49
|
let step2 = tmpFilename();
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
const primaryMonitor = monitors.find(monitor => monitor.isPrimary);
|
|
52
|
-
const image = await primaryMonitor.captureImage(); // Capture the image asynchronously
|
|
53
|
-
const buffer = await image.toPng(); // Convert the image to PNG format
|
|
54
|
-
fs.writeFileSync(step1, buffer); // Save the image to a file
|
|
51
|
+
await screenshot({ filename: step1, format: "png" });
|
|
55
52
|
|
|
56
53
|
// Fetch the mouse position
|
|
57
54
|
const mousePos = robot.getMousePos();
|
|
@@ -114,25 +111,9 @@ const platform = () => {
|
|
|
114
111
|
return platform;
|
|
115
112
|
};
|
|
116
113
|
|
|
117
|
-
// Import get-windows using dynamic import for ES module compatibility
|
|
118
|
-
let activeWindowFn = null;
|
|
119
|
-
const initializeActiveWindow = async () => {
|
|
120
|
-
if (!activeWindowFn) {
|
|
121
|
-
const { activeWindow } = await import('get-windows');
|
|
122
|
-
activeWindowFn = activeWindow;
|
|
123
|
-
}
|
|
124
|
-
return activeWindowFn;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
114
|
// this is the focused window
|
|
128
115
|
const activeWin = async () => {
|
|
129
|
-
|
|
130
|
-
const activeWindow = await initializeActiveWindow();
|
|
131
|
-
return await activeWindow();
|
|
132
|
-
} catch (error) {
|
|
133
|
-
console.error('Error getting active window:', error);
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
116
|
+
return await activeWindow();
|
|
136
117
|
};
|
|
137
118
|
|
|
138
119
|
const getMousePosition = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0-test.0",
|
|
4
4
|
"description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"license": "ISC",
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@electerm/strip-ansi": "^1.0.0",
|
|
20
|
+
"active-win": "^8.2.1",
|
|
20
21
|
"axios": "^1.7.7",
|
|
21
22
|
"chalk": "^4.1.2",
|
|
22
23
|
"cli-progress": "^3.12.0",
|
|
@@ -24,7 +25,6 @@
|
|
|
24
25
|
"decompress": "^4.2.1",
|
|
25
26
|
"dotenv": "^16.4.5",
|
|
26
27
|
"electron": "^33.0.2",
|
|
27
|
-
"get-windows": "^9.2.0",
|
|
28
28
|
"jimp": "^0.22.12",
|
|
29
29
|
"js-yaml": "^4.1.0",
|
|
30
30
|
"mac-screen-capture-permissions": "^2.1.0",
|
|
@@ -34,13 +34,14 @@
|
|
|
34
34
|
"marky": "^1.2.5",
|
|
35
35
|
"node-ipc": "^12.0.0",
|
|
36
36
|
"node-notifier": "^10.0.1",
|
|
37
|
-
"node-screenshots": "0.2.1",
|
|
38
37
|
"odiff-bin": "^3.1.2",
|
|
39
38
|
"prompts": "^2.4.2",
|
|
40
39
|
"remark-parse": "^11.0.0",
|
|
40
|
+
"rimraf": "^5.0.5",
|
|
41
41
|
"robotjs": "^0.6.0",
|
|
42
42
|
"sanitize-filename": "^1.6.3",
|
|
43
43
|
"say": "^0.16.0",
|
|
44
|
+
"screenshot-desktop": "^1.15.0",
|
|
44
45
|
"semver": "^7.6.2",
|
|
45
46
|
"sharp": "^0.33.5",
|
|
46
47
|
"systeminformation": "^5.23.5",
|
|
@@ -48,19 +49,16 @@
|
|
|
48
49
|
"uuid": "^10.0.0",
|
|
49
50
|
"winston": "^3.13.0"
|
|
50
51
|
},
|
|
51
|
-
"overrides": {
|
|
52
|
-
"glob": "^11.0.1",
|
|
53
|
-
"rimraf": "^5.0.10"
|
|
54
|
-
},
|
|
55
52
|
"devDependencies": {
|
|
56
53
|
"@eslint/js": "^9.10.0",
|
|
57
|
-
"chai": "^5.1.2",
|
|
58
54
|
"esbuild": "0.20.2",
|
|
59
55
|
"esbuild-plugin-fileloc": "^0.0.6",
|
|
60
56
|
"eslint": "^9.10.0",
|
|
61
57
|
"globals": "^15.9.0",
|
|
62
|
-
"mocha": "^10.8.2",
|
|
63
58
|
"node-addon-api": "^8.0.0",
|
|
59
|
+
"node-gyp": "^10.1.0",
|
|
60
|
+
"mocha": "^10.8.2",
|
|
61
|
+
"chai": "^5.1.2",
|
|
64
62
|
"prettier": "3.3.3"
|
|
65
63
|
},
|
|
66
64
|
"optionalDependencies": {
|
package/shot.png
ADDED
|
Binary file
|
package/test.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const screenshot = require("screenshot-desktop");
|
|
2
|
+
const robot = require("robotjs");
|
|
3
|
+
const Jimp = require('jimp');
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
async function go () {
|
|
7
|
+
await screenshot({ filename: "shot.png", format: "png" });
|
|
8
|
+
let pos = robot.getMousePos()
|
|
9
|
+
console.log(pos);
|
|
10
|
+
let im = await Jimp.read("shot.png");
|
|
11
|
+
|
|
12
|
+
// write im as a base64 string js file that can be imported in js
|
|
13
|
+
const base64 = await im.getBase64Async(Jimp.MIME_PNG);
|
|
14
|
+
fs.writeFileSync("pointer_image.js", `module.exports = \n"${base64}";\n`);
|
|
15
|
+
|
|
16
|
+
let pointer = await Jimp.read("./cursor.png");
|
|
17
|
+
console.log(im.getWidth(), im.getHeight());
|
|
18
|
+
im.composite(pointer, pos.x * 2 , pos.y * 2);
|
|
19
|
+
|
|
20
|
+
im.write("shot.png");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
go();
|
|
24
|
+
|
|
25
|
+
const screenshot = require("screenshot-desktop");
|
|
26
|
+
|
|
27
|
+
async function go () {
|
|
28
|
+
await screenshot({ filename: "shot.png", format: "png" });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
go();
|
package/test.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: 4.1.40
|
|
2
|
+
steps:
|
|
3
|
+
- prompt: Enter Password n stuff
|
|
4
|
+
commands:
|
|
5
|
+
- command: focus-application
|
|
6
|
+
name: Google Chrome
|
|
7
|
+
- command: hover-text
|
|
8
|
+
text: Username
|
|
9
|
+
description: username field
|
|
10
|
+
action: click
|
|
11
|
+
- command: type
|
|
12
|
+
text: ${TD_PASSWORD}
|
|
13
|
+
- command: hover-text
|
|
14
|
+
text: Password
|
|
15
|
+
description: Password field
|
|
16
|
+
action: click
|
|
17
|
+
- command: type
|
|
18
|
+
text: ${TD_PASSWORD}
|
package/.github/dependabot.yml
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
-
# package ecosystems to update and where the package manifests are located.
|
|
3
|
-
# Please see the documentation for all configuration options:
|
|
4
|
-
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
-
|
|
6
|
-
version: 2
|
|
7
|
-
updates:
|
|
8
|
-
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
-
directory: "/" # Location of package manifests
|
|
10
|
-
schedule:
|
|
11
|
-
interval: "weekly"
|