@cloudbeat/atlas-cli 0.2.0 → 0.4.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.
Files changed (41) hide show
  1. package/README.md +52 -19
  2. package/dist/api.js +70 -18
  3. package/dist/cli.js +34 -6
  4. package/dist/commands/analyze.js +118 -0
  5. package/dist/commands/capacity.js +99 -0
  6. package/dist/commands/docs.js +57 -0
  7. package/dist/commands/files.js +18 -4
  8. package/dist/commands/plan.js +100 -0
  9. package/dist/commands/record.js +11 -1
  10. package/dist/commands/results.js +13 -7
  11. package/dist/commands/schema.js +86 -0
  12. package/dist/commands/skills.js +111 -0
  13. package/dist/commands/tests.js +73 -0
  14. package/dist/docs/agent-setup.md +109 -0
  15. package/dist/docs/agent-tasks.md +169 -0
  16. package/dist/docs/ai-agents.md +90 -0
  17. package/dist/docs/auth.md +74 -0
  18. package/dist/docs/automation.md +150 -0
  19. package/dist/docs/ci.md +105 -0
  20. package/dist/docs/clusters.md +48 -0
  21. package/dist/docs/commands.md +155 -0
  22. package/dist/docs/files.md +86 -0
  23. package/dist/docs/generators.md +52 -0
  24. package/dist/docs/getting-started.md +85 -0
  25. package/dist/docs/index.json +110 -0
  26. package/dist/docs/projects.md +50 -0
  27. package/dist/docs/recorder.md +143 -0
  28. package/dist/docs/results.md +79 -0
  29. package/dist/docs/running.md +80 -0
  30. package/dist/docs/skills.md +88 -0
  31. package/dist/docs/test-plans.md +92 -0
  32. package/dist/docs/troubleshooting.md +79 -0
  33. package/dist/plan.js +250 -0
  34. package/dist/skills/AGENTS.md +70 -0
  35. package/dist/skills/atlas-analyze/SKILL.md +104 -0
  36. package/dist/skills/atlas-correlate/SKILL.md +89 -0
  37. package/dist/skills/atlas-load-test/SKILL.md +154 -0
  38. package/dist/skills/atlas-record/SKILL.md +167 -0
  39. package/dist/skills/atlas-run/SKILL.md +109 -0
  40. package/dist/skills/atlas-test-plan/SKILL.md +281 -0
  41. package/package.json +5 -3
@@ -96,6 +96,7 @@ function registerFiles(program) {
96
96
  .command('upload <files...>')
97
97
  .description('Upload files into the project (replaces same-named files)')
98
98
  .option('-p, --project <name>', 'project name')
99
+ .option('--json', 'machine-readable output')
99
100
  .action(async (paths, opts) => {
100
101
  const api = api_1.AtlasApi.fromConfig();
101
102
  const project = await api.projectByName(projectName(opts.project));
@@ -103,8 +104,13 @@ function registerFiles(program) {
103
104
  if (missing.length)
104
105
  throw new ui_1.CliError(`File(s) not found: ${missing.join(', ')}`, ui_1.EXIT.USAGE);
105
106
  await api.uploadFiles(project.id, paths);
106
- for (const p of paths) {
107
- ui_1.log.ok(`${path_1.default.basename(p)} → ${project.name} (${humanSize(fs_1.default.statSync(p).size)})`);
107
+ const uploaded = paths.map(p => ({ filename: path_1.default.basename(p), bytes: fs_1.default.statSync(p).size }));
108
+ if (opts.json) {
109
+ (0, ui_1.json)({ project: project.name, uploaded });
110
+ return;
111
+ }
112
+ for (const f of uploaded) {
113
+ ui_1.log.ok(`${f.filename} → ${project.name} (${humanSize(f.bytes)})`);
108
114
  }
109
115
  });
110
116
  files
@@ -112,19 +118,24 @@ function registerFiles(program) {
112
118
  .description('Download one file from the project')
113
119
  .option('-p, --project <name>', 'project name')
114
120
  .option('-o, --out <path>', 'where to write it (default: the filename, in cwd)')
121
+ .option('--json', 'machine-readable output')
115
122
  .action(async (filename, opts) => {
116
123
  const api = api_1.AtlasApi.fromConfig();
117
124
  const project = await api.projectByName(projectName(opts.project));
118
125
  const buf = await api.downloadFile(project.id, filename);
119
126
  const dest = opts.out ?? filename;
120
127
  fs_1.default.writeFileSync(dest, buf);
121
- ui_1.log.ok(`${filename} → ${dest} (${humanSize(buf.length)})`);
128
+ if (opts.json)
129
+ (0, ui_1.json)({ project: project.name, filename, path: path_1.default.resolve(dest), bytes: buf.length });
130
+ else
131
+ ui_1.log.ok(`${filename} → ${dest} (${humanSize(buf.length)})`);
122
132
  });
123
133
  files
124
134
  .command('rm <filename>')
125
135
  .description('Delete a file from the project')
126
136
  .option('-p, --project <name>', 'project name')
127
137
  .option('-y, --yes', 'skip the confirmation prompt')
138
+ .option('--json', 'machine-readable output')
128
139
  .action(async (filename, opts) => {
129
140
  const api = api_1.AtlasApi.fromConfig();
130
141
  const project = await api.projectByName(projectName(opts.project));
@@ -143,7 +154,10 @@ function registerFiles(program) {
143
154
  }
144
155
  }
145
156
  await api.deleteFile(project.id, filename);
146
- ui_1.log.ok(`Deleted ${filename} from ${project.name}`);
157
+ if (opts.json)
158
+ (0, ui_1.json)({ project: project.name, deleted: filename });
159
+ else
160
+ ui_1.log.ok(`Deleted ${filename} from ${project.name}`);
147
161
  });
148
162
  }
149
163
  //# sourceMappingURL=files.js.map
@@ -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
@@ -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
  }
@@ -108,20 +108,26 @@ function registerResults(program) {
108
108
  program
109
109
  .command('stop <testId>')
110
110
  .description('Stop a running test')
111
- .action(async (testId) => {
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
- ui_1.log.warn(`Stopped on ${res.ok} server(s), but could not confirm all of them.`);
118
- if (res.unreachable?.length)
119
- ui_1.log.warn(`unreachable: ${res.unreachable.join(', ')}`);
120
- if (res.stillRunning?.length)
121
- ui_1.log.warn(`still running: ${res.stillRunning.join(', ')}`);
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
- ui_1.log.ok(`Stopped ${testId} on ${res.ok} server(s).`);
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)