@cloudbeat/atlas-cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -19
- package/dist/api.js +20 -0
- package/dist/cli.js +34 -6
- package/dist/commands/analyze.js +118 -0
- package/dist/commands/capacity.js +104 -0
- package/dist/commands/docs.js +57 -0
- package/dist/commands/files.js +18 -4
- package/dist/commands/plan.js +100 -0
- package/dist/commands/record.js +11 -1
- package/dist/commands/results.js +13 -7
- package/dist/commands/schema.js +86 -0
- package/dist/commands/skills.js +111 -0
- package/dist/commands/tests.js +73 -0
- package/dist/docs/agent-setup.md +109 -0
- package/dist/docs/agent-tasks.md +169 -0
- package/dist/docs/ai-agents.md +90 -0
- package/dist/docs/auth.md +74 -0
- package/dist/docs/automation.md +150 -0
- package/dist/docs/ci.md +105 -0
- package/dist/docs/clusters.md +48 -0
- package/dist/docs/commands.md +155 -0
- package/dist/docs/files.md +86 -0
- package/dist/docs/generators.md +52 -0
- package/dist/docs/getting-started.md +85 -0
- package/dist/docs/index.json +110 -0
- package/dist/docs/projects.md +50 -0
- package/dist/docs/recorder.md +143 -0
- package/dist/docs/results.md +79 -0
- package/dist/docs/running.md +80 -0
- package/dist/docs/skills.md +88 -0
- package/dist/docs/test-plans.md +92 -0
- package/dist/docs/troubleshooting.md +79 -0
- package/dist/plan.js +250 -0
- package/dist/skills/AGENTS.md +70 -0
- package/dist/skills/atlas-analyze/SKILL.md +104 -0
- package/dist/skills/atlas-correlate/SKILL.md +89 -0
- package/dist/skills/atlas-load-test/SKILL.md +154 -0
- package/dist/skills/atlas-record/SKILL.md +167 -0
- package/dist/skills/atlas-run/SKILL.md +109 -0
- package/dist/skills/atlas-test-plan/SKILL.md +281 -0
- package/package.json +5 -3
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerPlan = registerPlan;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const api_1 = require("../api");
|
|
11
|
+
const config_1 = require("../config");
|
|
12
|
+
const ui_1 = require("../ui");
|
|
13
|
+
const plan_1 = require("../plan");
|
|
14
|
+
/** Project comes from --project, ATLAS_PROJECT, or the saved default. */
|
|
15
|
+
function projectName(flag) {
|
|
16
|
+
return (0, config_1.loadConfig)({ project: flag }).project;
|
|
17
|
+
}
|
|
18
|
+
function printReport(name, report) {
|
|
19
|
+
ui_1.log.info(chalk_1.default.bold(name));
|
|
20
|
+
for (const g of report.threadGroups) {
|
|
21
|
+
const state = g.enabled ? '' : chalk_1.default.dim(' (disabled)');
|
|
22
|
+
const size = [g.threads && `${g.threads} users`, g.loops && `${g.loops} loops`]
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join(', ');
|
|
25
|
+
console.log(` ${chalk_1.default.cyan('group')} ${g.name}${state}${size ? chalk_1.default.dim(` ${size}`) : ''}`);
|
|
26
|
+
}
|
|
27
|
+
for (const f of report.dataFiles) {
|
|
28
|
+
const mark = f.uploaded === false ? chalk_1.default.red('missing') : f.uploaded ? chalk_1.default.green('uploaded') : chalk_1.default.dim('?');
|
|
29
|
+
console.log(` ${chalk_1.default.cyan('data ')} ${f.filename} ${mark}`);
|
|
30
|
+
}
|
|
31
|
+
if (report.issues.length)
|
|
32
|
+
console.log();
|
|
33
|
+
for (const issue of report.issues) {
|
|
34
|
+
const line = `${issue.message}${issue.fix ? chalk_1.default.dim(` → ${issue.fix}`) : ''}`;
|
|
35
|
+
if (issue.severity === 'error')
|
|
36
|
+
ui_1.log.error(line);
|
|
37
|
+
else if (issue.severity === 'warning')
|
|
38
|
+
ui_1.log.warn(line);
|
|
39
|
+
else
|
|
40
|
+
ui_1.log.dim(` ${issue.message}`);
|
|
41
|
+
}
|
|
42
|
+
console.log();
|
|
43
|
+
if (report.ok)
|
|
44
|
+
ui_1.log.ok('This plan can run.');
|
|
45
|
+
else
|
|
46
|
+
ui_1.log.error('This plan would not produce results as it stands.');
|
|
47
|
+
}
|
|
48
|
+
function registerPlan(program) {
|
|
49
|
+
const plan = program.command('plan').description('Work with a test plan file');
|
|
50
|
+
plan
|
|
51
|
+
.command('check [file]')
|
|
52
|
+
.description('Check a plan will actually run and report — locally, or one in a project')
|
|
53
|
+
.option('-p, --project <name>', 'project to check the plan against (and to fetch it from)')
|
|
54
|
+
.option('--jmx <filename>', 'which plan in the project (default: the project\'s)')
|
|
55
|
+
.option('--json', 'machine-readable output')
|
|
56
|
+
.action(async (file, opts) => {
|
|
57
|
+
const name = projectName(opts.project);
|
|
58
|
+
let content;
|
|
59
|
+
let label;
|
|
60
|
+
let uploaded;
|
|
61
|
+
if (file) {
|
|
62
|
+
if (!fs_1.default.existsSync(file))
|
|
63
|
+
throw new ui_1.CliError(`No such file: ${file}`, ui_1.EXIT.USAGE);
|
|
64
|
+
content = fs_1.default.readFileSync(file, 'utf8');
|
|
65
|
+
label = path_1.default.basename(file);
|
|
66
|
+
// A local file can still be checked against a project's uploads — that
|
|
67
|
+
// is the interesting question before uploading it.
|
|
68
|
+
if (name) {
|
|
69
|
+
const api = api_1.AtlasApi.fromConfig();
|
|
70
|
+
const project = await api.projectByName(name);
|
|
71
|
+
uploaded = (await api.files(project.id)).map(f => f.filename);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (!name) {
|
|
76
|
+
throw new ui_1.CliError('Pass a file to check, or -p <project> to check the project\'s plan.', ui_1.EXIT.USAGE);
|
|
77
|
+
}
|
|
78
|
+
const api = api_1.AtlasApi.fromConfig();
|
|
79
|
+
const project = await api.projectByName(name);
|
|
80
|
+
const files = await api.files(project.id);
|
|
81
|
+
uploaded = files.map(f => f.filename);
|
|
82
|
+
const jmx = opts.jmx ?? project.jmx_filename ?? undefined;
|
|
83
|
+
if (!jmx) {
|
|
84
|
+
throw new ui_1.CliError(`Project "${project.name}" has no test plan. Upload one first.`, ui_1.EXIT.USAGE);
|
|
85
|
+
}
|
|
86
|
+
content = (await api.downloadFile(project.id, jmx)).toString('utf8');
|
|
87
|
+
label = jmx;
|
|
88
|
+
}
|
|
89
|
+
const report = (0, plan_1.checkPlan)(content, uploaded);
|
|
90
|
+
if (opts.json)
|
|
91
|
+
(0, ui_1.json)({ plan: label, project: name ?? null, ...report });
|
|
92
|
+
else
|
|
93
|
+
printReport(label, report);
|
|
94
|
+
// An unrunnable plan is a usage problem the caller must fix, and CI should
|
|
95
|
+
// stop on it rather than proceed to a run that reports nothing.
|
|
96
|
+
if (!report.ok)
|
|
97
|
+
throw new ui_1.CliError(`${label} would not produce results.`, ui_1.EXIT.USAGE);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=plan.js.map
|
package/dist/commands/record.js
CHANGED
|
@@ -58,6 +58,7 @@ function registerRecord(program) {
|
|
|
58
58
|
.option('--profile <dir>', 'browser profile directory for a persistent session')
|
|
59
59
|
.option('--upload', 'upload the recorded .jmx into the project when done')
|
|
60
60
|
.option('-p, --project <name>', 'project to upload into (with --upload)')
|
|
61
|
+
.option('--json', 'machine-readable output')
|
|
61
62
|
.action(async (url, opts) => {
|
|
62
63
|
// Fail on an unusable --upload before spending a recording session on it.
|
|
63
64
|
let project = null;
|
|
@@ -94,9 +95,18 @@ function registerRecord(program) {
|
|
|
94
95
|
if (!fs_1.default.existsSync(opts.out)) {
|
|
95
96
|
throw new ui_1.CliError(`Recorder finished but ${opts.out} was not written.`, ui_1.EXIT.ERROR);
|
|
96
97
|
}
|
|
98
|
+
if (project)
|
|
99
|
+
await api_1.AtlasApi.fromConfig().uploadFiles(project.id, [opts.out]);
|
|
100
|
+
if (opts.json) {
|
|
101
|
+
(0, ui_1.json)({
|
|
102
|
+
out: path_1.default.resolve(opts.out),
|
|
103
|
+
filename: path_1.default.basename(opts.out),
|
|
104
|
+
uploaded: project ? project.name : null,
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
97
108
|
ui_1.log.ok(`Recorded ${chalk_1.default.bold(opts.out)}`);
|
|
98
109
|
if (project) {
|
|
99
|
-
await api_1.AtlasApi.fromConfig().uploadFiles(project.id, [opts.out]);
|
|
100
110
|
ui_1.log.ok(`Uploaded ${path_1.default.basename(opts.out)} → ${project.name}`);
|
|
101
111
|
ui_1.log.dim(`run it with: atlas run --project ${project.name} --jmx ${path_1.default.basename(opts.out)}`);
|
|
102
112
|
}
|
package/dist/commands/results.js
CHANGED
|
@@ -108,20 +108,26 @@ function registerResults(program) {
|
|
|
108
108
|
program
|
|
109
109
|
.command('stop <testId>')
|
|
110
110
|
.description('Stop a running test')
|
|
111
|
-
.
|
|
111
|
+
.option('--json', 'machine-readable output')
|
|
112
|
+
.action(async (testId, opts) => {
|
|
112
113
|
const api = api_1.AtlasApi.fromConfig();
|
|
113
114
|
const res = await api.stop(testId);
|
|
115
|
+
if (opts.json)
|
|
116
|
+
(0, ui_1.json)({ testId, ...res });
|
|
114
117
|
if (res.unreachable?.length || res.stillRunning?.length) {
|
|
115
118
|
// The API only marks a server stopped once it confirms the process is
|
|
116
119
|
// gone; anything else must be surfaced, not rounded up to success.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
if (!opts.json) {
|
|
121
|
+
ui_1.log.warn(`Stopped on ${res.ok} server(s), but could not confirm all of them.`);
|
|
122
|
+
if (res.unreachable?.length)
|
|
123
|
+
ui_1.log.warn(`unreachable: ${res.unreachable.join(', ')}`);
|
|
124
|
+
if (res.stillRunning?.length)
|
|
125
|
+
ui_1.log.warn(`still running: ${res.stillRunning.join(', ')}`);
|
|
126
|
+
}
|
|
122
127
|
throw new ui_1.CliError('Run not fully stopped — retry when those servers are reachable.', ui_1.EXIT.ERROR);
|
|
123
128
|
}
|
|
124
|
-
|
|
129
|
+
if (!opts.json)
|
|
130
|
+
ui_1.log.ok(`Stopped ${testId} on ${res.ok} server(s).`);
|
|
125
131
|
});
|
|
126
132
|
}
|
|
127
133
|
//# sourceMappingURL=results.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerSchema = registerSchema;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ui_1 = require("../ui");
|
|
9
|
+
function optionSchema(o) {
|
|
10
|
+
// Commander's `required`/`optional` describe the option's VALUE, not whether
|
|
11
|
+
// the option itself must be passed — name them so an agent can't read them
|
|
12
|
+
// as "this flag is mandatory" and start passing --json <something>.
|
|
13
|
+
return {
|
|
14
|
+
flags: o.flags,
|
|
15
|
+
description: o.description,
|
|
16
|
+
takesValue: !!o.required || !!o.optional,
|
|
17
|
+
valueOptional: !!o.optional,
|
|
18
|
+
variadic: !!o.variadic,
|
|
19
|
+
...(o.defaultValue !== undefined ? { default: o.defaultValue } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function argumentSchema(a) {
|
|
23
|
+
return {
|
|
24
|
+
name: typeof a.name === 'function' ? a.name() : String(a.name ?? ''),
|
|
25
|
+
description: a.description ?? '',
|
|
26
|
+
required: !!a.required,
|
|
27
|
+
variadic: !!a.variadic,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function commandSchema(cmd) {
|
|
31
|
+
// registeredArguments is Commander 11+; keep the older private field as a
|
|
32
|
+
// fallback so a dependency bump can't silently empty the argument list.
|
|
33
|
+
const args = cmd.registeredArguments ?? cmd._args ?? [];
|
|
34
|
+
return {
|
|
35
|
+
name: cmd.name(),
|
|
36
|
+
description: cmd.description(),
|
|
37
|
+
aliases: cmd.aliases(),
|
|
38
|
+
arguments: args.map(argumentSchema),
|
|
39
|
+
options: cmd.options.map(optionSchema),
|
|
40
|
+
commands: cmd.commands.map(c => commandSchema(c)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function printTree(cmds, prefix) {
|
|
44
|
+
for (const c of cmds) {
|
|
45
|
+
const args = c.arguments.map(a => (a.required ? `<${a.name}>` : `[${a.name}]`)).join(' ');
|
|
46
|
+
console.log(`${prefix}${chalk_1.default.bold(c.name)}${args ? ' ' + args : ''} ${chalk_1.default.dim(c.description)}`);
|
|
47
|
+
for (const o of c.options)
|
|
48
|
+
console.log(`${prefix} ${o.flags} ${chalk_1.default.dim(o.description)}`);
|
|
49
|
+
printTree(c.commands, prefix + ' ');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function registerSchema(program) {
|
|
53
|
+
program
|
|
54
|
+
.command('schema')
|
|
55
|
+
.description('Describe every command, argument and option — for scripts and AI agents')
|
|
56
|
+
.option('--json', 'machine-readable output')
|
|
57
|
+
.action((opts) => {
|
|
58
|
+
const { version } = require('../../package.json');
|
|
59
|
+
const schema = {
|
|
60
|
+
name: program.name(),
|
|
61
|
+
version,
|
|
62
|
+
description: program.description(),
|
|
63
|
+
exitCodes: {
|
|
64
|
+
[ui_1.EXIT.OK]: 'success',
|
|
65
|
+
[ui_1.EXIT.GATE_FAILED]: 'a --gate threshold was breached',
|
|
66
|
+
[ui_1.EXIT.USAGE]: 'usage error: bad flags, unknown project, not authenticated',
|
|
67
|
+
[ui_1.EXIT.ERROR]: 'the run failed, timed out, or the API was unreachable',
|
|
68
|
+
},
|
|
69
|
+
env: {
|
|
70
|
+
ATLAS_URL: 'control plane URL (default https://atlas.cloudbeat.io)',
|
|
71
|
+
ATLAS_TOKEN: 'API token — create one on the Profile page',
|
|
72
|
+
ATLAS_PROJECT: 'default project for --project',
|
|
73
|
+
ATLAS_DEBUG: 'print stack traces for unexpected failures',
|
|
74
|
+
},
|
|
75
|
+
commands: program.commands.map(c => commandSchema(c)),
|
|
76
|
+
};
|
|
77
|
+
if (opts.json) {
|
|
78
|
+
(0, ui_1.json)(schema);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
ui_1.log.info(`${chalk_1.default.bold(schema.name)} ${schema.version}`);
|
|
82
|
+
printTree(schema.commands, '');
|
|
83
|
+
ui_1.log.dim('Use --json for the machine-readable form.');
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerSkills = registerSkills;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const ui_1 = require("../ui");
|
|
12
|
+
/**
|
|
13
|
+
* Installing the guidance an AI assistant needs to drive Atlas properly.
|
|
14
|
+
*
|
|
15
|
+
* The content is written once, vendor-neutral, and shipped in the package. This
|
|
16
|
+
* copies it where a given assistant looks: skills under .claude/skills for
|
|
17
|
+
* Claude Code, AGENTS.md for everything that reads that convention.
|
|
18
|
+
*/
|
|
19
|
+
/** Bundled by scripts/bundle-skills.js at build time. */
|
|
20
|
+
const SKILLS_DIR = path_1.default.join(__dirname, '..', 'skills');
|
|
21
|
+
function bundledSkills() {
|
|
22
|
+
if (!fs_1.default.existsSync(SKILLS_DIR))
|
|
23
|
+
return [];
|
|
24
|
+
return fs_1.default
|
|
25
|
+
.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
26
|
+
.filter(e => e.isDirectory() && fs_1.default.existsSync(path_1.default.join(SKILLS_DIR, e.name, 'SKILL.md')))
|
|
27
|
+
.map(e => e.name)
|
|
28
|
+
.sort();
|
|
29
|
+
}
|
|
30
|
+
/** Copy one file. An existing file is left alone unless --force: it may have
|
|
31
|
+
* been edited for the project, and silently replacing that is worse than
|
|
32
|
+
* leaving it slightly out of date. */
|
|
33
|
+
function place(from, to, force) {
|
|
34
|
+
if (fs_1.default.existsSync(to) && !force)
|
|
35
|
+
return 'skipped';
|
|
36
|
+
fs_1.default.mkdirSync(path_1.default.dirname(to), { recursive: true });
|
|
37
|
+
fs_1.default.copyFileSync(from, to);
|
|
38
|
+
return 'written';
|
|
39
|
+
}
|
|
40
|
+
function registerSkills(program) {
|
|
41
|
+
const skills = program
|
|
42
|
+
.command('skills')
|
|
43
|
+
.description('Install the Atlas guidance for AI coding assistants');
|
|
44
|
+
skills
|
|
45
|
+
.command('list')
|
|
46
|
+
.description('Show the skills bundled with this CLI')
|
|
47
|
+
.option('--json', 'machine-readable output')
|
|
48
|
+
.action((opts) => {
|
|
49
|
+
const names = bundledSkills();
|
|
50
|
+
if (opts.json) {
|
|
51
|
+
(0, ui_1.json)({ skills: names.map(name => ({ name, source: path_1.default.join(SKILLS_DIR, name, 'SKILL.md') })) });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (names.length === 0) {
|
|
55
|
+
ui_1.log.warn('No skills are bundled with this build.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const name of names)
|
|
59
|
+
console.log(` ${chalk_1.default.cyan(name)}`);
|
|
60
|
+
});
|
|
61
|
+
skills
|
|
62
|
+
.command('install')
|
|
63
|
+
.description('Copy the skills into this project (or your home directory)')
|
|
64
|
+
.option('--user', 'install for every project, under your home directory')
|
|
65
|
+
.option('--project', 'install into the current project (default)')
|
|
66
|
+
.option('--dir <path>', 'install somewhere else entirely')
|
|
67
|
+
.option('--force', 'overwrite files that are already there')
|
|
68
|
+
.option('--json', 'machine-readable output')
|
|
69
|
+
.action((opts) => {
|
|
70
|
+
const names = bundledSkills();
|
|
71
|
+
if (names.length === 0) {
|
|
72
|
+
throw new ui_1.CliError('No skills are bundled with this build.', ui_1.EXIT.ERROR);
|
|
73
|
+
}
|
|
74
|
+
const base = opts.dir
|
|
75
|
+
? path_1.default.resolve(opts.dir)
|
|
76
|
+
: opts.user
|
|
77
|
+
? path_1.default.join(os_1.default.homedir(), '.claude')
|
|
78
|
+
: path_1.default.join(process.cwd(), '.claude');
|
|
79
|
+
const installed = [];
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
const to = path_1.default.join(base, 'skills', name, 'SKILL.md');
|
|
82
|
+
installed.push({
|
|
83
|
+
name,
|
|
84
|
+
path: to,
|
|
85
|
+
status: place(path_1.default.join(SKILLS_DIR, name, 'SKILL.md'), to, !!opts.force),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// AGENTS.md is the vendor-neutral half: Copilot, Cursor and others read it
|
|
89
|
+
// even though they know nothing about skills. Project scope only — a home
|
|
90
|
+
// directory is not a project root.
|
|
91
|
+
let agents;
|
|
92
|
+
const agentsSource = path_1.default.join(SKILLS_DIR, 'AGENTS.md');
|
|
93
|
+
if (!opts.user && fs_1.default.existsSync(agentsSource)) {
|
|
94
|
+
const to = path_1.default.join(opts.dir ? base : process.cwd(), 'AGENTS.md');
|
|
95
|
+
agents = { name: 'AGENTS.md', path: to, status: place(agentsSource, to, !!opts.force) };
|
|
96
|
+
installed.push(agents);
|
|
97
|
+
}
|
|
98
|
+
if (opts.json) {
|
|
99
|
+
(0, ui_1.json)({ base, installed });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const item of installed) {
|
|
103
|
+
if (item.status === 'written')
|
|
104
|
+
ui_1.log.ok(`${item.name} → ${item.path}`);
|
|
105
|
+
else
|
|
106
|
+
ui_1.log.dim(`${item.name} already present — left alone (use --force to replace)`);
|
|
107
|
+
}
|
|
108
|
+
ui_1.log.dim('Start a new assistant session to pick them up.');
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=skills.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerTests = registerTests;
|
|
7
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const api_1 = require("../api");
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const ui_1 = require("../ui");
|
|
12
|
+
/** Project comes from --project, ATLAS_PROJECT, or the saved default. */
|
|
13
|
+
function projectName(flag) {
|
|
14
|
+
const name = (0, config_1.loadConfig)({ project: flag }).project;
|
|
15
|
+
if (!name) {
|
|
16
|
+
throw new ui_1.CliError('No project. Pass --project <name>, set ATLAS_PROJECT, or run `atlas config set project <name>`.', ui_1.EXIT.USAGE);
|
|
17
|
+
}
|
|
18
|
+
return name;
|
|
19
|
+
}
|
|
20
|
+
/** "Jun 7, 08:19 PM" — the same stamp the dashboard's test list shows. */
|
|
21
|
+
function started(iso) {
|
|
22
|
+
return new Date(iso).toLocaleString(undefined, {
|
|
23
|
+
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function printTests(tests) {
|
|
27
|
+
const table = new cli_table3_1.default({
|
|
28
|
+
head: ['TEST ID', 'STATUS', 'STARTED', 'USERS', 'REQUESTS', 'ERR%'].map(h => chalk_1.default.dim(h)),
|
|
29
|
+
colAligns: ['left', 'left', 'left', 'right', 'right', 'right'],
|
|
30
|
+
style: { head: [], border: [] },
|
|
31
|
+
});
|
|
32
|
+
for (const t of tests) {
|
|
33
|
+
table.push([
|
|
34
|
+
t.testId,
|
|
35
|
+
t.status === 'running' ? chalk_1.default.green('● running') : chalk_1.default.dim('completed'),
|
|
36
|
+
started(t.startedAt),
|
|
37
|
+
String(t.maxUsers),
|
|
38
|
+
String(t.totalRequests),
|
|
39
|
+
t.errorPct === 0 ? chalk_1.default.green('0%') : chalk_1.default.yellow(`${t.errorPct}%`),
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
console.log(table.toString());
|
|
43
|
+
}
|
|
44
|
+
function registerTests(program) {
|
|
45
|
+
program
|
|
46
|
+
.command('tests')
|
|
47
|
+
.description('List a project\'s recent runs, most recent first')
|
|
48
|
+
.option('-p, --project <name>', 'project name')
|
|
49
|
+
.option('--limit <n>', 'show at most this many runs')
|
|
50
|
+
.option('--json', 'machine-readable output')
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
const api = api_1.AtlasApi.fromConfig();
|
|
53
|
+
const project = projectName(opts.project);
|
|
54
|
+
const { tests } = await api.tests(project);
|
|
55
|
+
const limit = opts.limit ? parseInt(opts.limit, 10) : undefined;
|
|
56
|
+
if (opts.limit && (!Number.isFinite(limit) || limit < 1)) {
|
|
57
|
+
throw new ui_1.CliError('--limit must be a positive integer', ui_1.EXIT.USAGE);
|
|
58
|
+
}
|
|
59
|
+
const shown = limit ? tests.slice(0, limit) : tests;
|
|
60
|
+
if (opts.json) {
|
|
61
|
+
(0, ui_1.json)({ project, tests: shown });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (shown.length === 0) {
|
|
65
|
+
// The dashboard's list only reaches back a week, so an empty result is
|
|
66
|
+
// "nothing recent", not "this project has never run".
|
|
67
|
+
ui_1.log.warn(`No runs reported for "${project}" in the last 7 days.`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
printTests(shown);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=tests.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Setting It Up
|
|
2
|
+
|
|
3
|
+
Two commands to install, one to sign in. The guidance is bundled with the CLI,
|
|
4
|
+
so there is nothing to download separately and nothing to keep in sync by hand.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install -g @cloudbeat/atlas-cli @cloudbeat/atlas-recorder
|
|
10
|
+
atlas login --token <your API token>
|
|
11
|
+
atlas skills install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Create the token on your **Profile** page in Atlas. A token acts as you, in one
|
|
15
|
+
account, so the agent can never reach further than you can.
|
|
16
|
+
|
|
17
|
+
`atlas skills install` arrived in 0.4.0. Check with `atlas --version`, and
|
|
18
|
+
upgrade with `npm install -g @cloudbeat/atlas-cli@latest`.
|
|
19
|
+
|
|
20
|
+
The recorder is a separate package because it downloads its own browser. Install
|
|
21
|
+
it when you want an agent to record scenarios; skip it in CI, where nothing
|
|
22
|
+
opens a browser.
|
|
23
|
+
|
|
24
|
+
## What gets installed
|
|
25
|
+
|
|
26
|
+
`atlas skills install` writes into the current project:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
.claude/skills/
|
|
30
|
+
atlas-load-test/SKILL.md the end-to-end flow
|
|
31
|
+
atlas-record/SKILL.md recording a scenario from a browser
|
|
32
|
+
atlas-test-plan/SKILL.md plans, data files, local debugging
|
|
33
|
+
atlas-correlate/SKILL.md per-session values: tokens, session ids
|
|
34
|
+
atlas-run/SKILL.md sizing, thresholds, CI
|
|
35
|
+
atlas-analyze/SKILL.md reading results, and spotting a broken test
|
|
36
|
+
AGENTS.md the same guidance, vendor-neutral
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
See [The Skills](/agents/skills) for what each one covers and when it loads.
|
|
40
|
+
|
|
41
|
+
**Start a new assistant session afterwards.** Skills and `AGENTS.md` are read
|
|
42
|
+
when a session starts, so an open session will not see them.
|
|
43
|
+
|
|
44
|
+
## Where to install it
|
|
45
|
+
|
|
46
|
+
| Command | Installs into | Use when |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `atlas skills install` | the current project | The repository whose performance you are testing — the usual choice, and it can be committed so your team shares it |
|
|
49
|
+
| `atlas skills install --user` | your home directory | You want every project on your machine to have it |
|
|
50
|
+
| `atlas skills install --dir <path>` | anywhere | Unusual layouts, or a shared checkout |
|
|
51
|
+
|
|
52
|
+
Existing files are **left alone**, because you may have edited them for your
|
|
53
|
+
project. Pass `--force` to replace them:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
atlas skills install --force
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
To see what a build carries before installing anything:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
atlas skills list
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Claude Code
|
|
66
|
+
|
|
67
|
+
Nothing else to do. Open Claude Code in the project and ask for a load test —
|
|
68
|
+
the relevant skill loads by itself when the request calls for it. You can
|
|
69
|
+
confirm they are there with `/skills`.
|
|
70
|
+
|
|
71
|
+
Committing `.claude/skills/` to your repository means everyone on the team gets
|
|
72
|
+
the same behaviour, and reviews changes to it like any other file.
|
|
73
|
+
|
|
74
|
+
## Copilot, Cursor, Codex and friends
|
|
75
|
+
|
|
76
|
+
These read `AGENTS.md` from the project root, which `atlas skills install` writes
|
|
77
|
+
for you. No configuration, no extension. Commit it alongside your code.
|
|
78
|
+
|
|
79
|
+
If your assistant reads a differently named file, copy it:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
cp AGENTS.md CLAUDE.md # or .cursorrules, .github/copilot-instructions.md
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Keeping it current
|
|
86
|
+
|
|
87
|
+
The guidance ships inside the CLI, so upgrading the CLI brings new guidance with
|
|
88
|
+
it:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
npm install -g @cloudbeat/atlas-cli@latest
|
|
92
|
+
atlas skills install --force
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Worth doing when you upgrade, because the guidance tracks the commands. If you
|
|
96
|
+
have edited a skill for your own project, diff before overwriting.
|
|
97
|
+
|
|
98
|
+
## Check it works
|
|
99
|
+
|
|
100
|
+
Ask your assistant something that needs Atlas but changes nothing:
|
|
101
|
+
|
|
102
|
+
> What can I load test here, and how much capacity do I have?
|
|
103
|
+
|
|
104
|
+
You should see it run `atlas projects` and `atlas capacity`, then answer with
|
|
105
|
+
your real projects and machine count. If it improvises shell commands or asks
|
|
106
|
+
you for infrastructure credentials, the guidance has not loaded — start a new
|
|
107
|
+
session and check `.claude/skills/` or `AGENTS.md` is present.
|
|
108
|
+
|
|
109
|
+
[What it can do →](/agents/capabilities)
|