codymaster 4.6.0 → 5.2.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/CHANGELOG.md +74 -8
- package/README.md +192 -95
- package/dist/advisory-handoff.js +89 -0
- package/dist/advisory-report.js +105 -0
- package/dist/browse-server.js +251 -0
- package/dist/cli/command-registry.js +34 -0
- package/dist/cli/commands/agent.js +120 -0
- package/dist/cli/commands/bench.js +69 -0
- package/dist/cli/commands/brain.js +108 -0
- package/dist/cli/commands/dashboard.js +93 -0
- package/dist/cli/commands/design-studio.js +111 -0
- package/dist/cli/commands/distro.js +25 -0
- package/dist/cli/commands/engineering.js +596 -0
- package/dist/cli/commands/evolve.js +123 -0
- package/dist/cli/commands/mcp-serve.js +104 -0
- package/dist/cli/commands/project.js +324 -0
- package/dist/cli/commands/skill-chain.js +269 -0
- package/dist/cli/commands/system.js +89 -0
- package/dist/cli/commands/task.js +254 -0
- package/dist/cli/update-check.js +83 -0
- package/dist/cm-config.js +92 -0
- package/dist/cm-suggest.js +77 -0
- package/dist/codybench/judges/automated.js +31 -0
- package/dist/codybench/runners/claude-code.js +32 -0
- package/dist/codybench/suites/memory-retention.js +85 -0
- package/dist/codybench/suites/tdd-regression.js +35 -0
- package/dist/codybench/suites/token-efficiency.js +55 -0
- package/dist/codybench/types.js +2 -0
- package/dist/context-db.js +157 -0
- package/dist/continuity.js +2 -6
- package/dist/distro-validate.js +54 -0
- package/dist/execution-analyzer.js +138 -0
- package/dist/guardian-core.js +74 -0
- package/dist/index.js +36 -2759
- package/dist/indexer/skills-lib.js +533 -0
- package/dist/indexer/skills-map.js +1374 -0
- package/dist/indexer/skills.js +16 -0
- package/dist/learning-promoter.js +246 -0
- package/dist/mcp-context-server.js +289 -1
- package/dist/mcp-skills-tools.js +81 -0
- package/dist/retro-summary.js +70 -0
- package/dist/second-opinion-providers.js +79 -0
- package/dist/skill-chain.js +63 -1
- package/dist/skill-evolver.js +456 -0
- package/dist/skill-execution-cache.js +254 -0
- package/dist/smart-brain-router.js +184 -0
- package/dist/sprint-pipeline.js +228 -0
- package/dist/storage-backend.js +14 -67
- package/dist/token-budget.js +88 -0
- package/dist/utils/cli-utils.js +76 -0
- package/dist/utils/skill-utils.js +32 -0
- package/package.json +17 -7
- package/scripts/build-skills.mjs +51 -0
- package/scripts/gate-0-repo-hygiene.js +75 -0
- package/scripts/postinstall.js +34 -28
- package/scripts/security-scan.js +1 -1
- package/scripts/validate-skills.mjs +42 -0
- package/skills/CLAUDE.md +2 -7
- package/skills/_shared/helpers.md +2 -8
- package/skills/cm-ads-tracker/SKILL.md +3 -6
- package/skills/cm-browse/SKILL.md +34 -0
- package/skills/cm-conductor-worktrees/SKILL.md +28 -0
- package/skills/cm-content-factory/SKILL.md +1 -1
- package/skills/cm-content-factory/landing/docs/content/changelog.md +36 -0
- package/skills/cm-content-factory/landing/docs/content/deployment.md +46 -0
- package/skills/cm-content-factory/landing/docs/content/execution-flow.md +67 -0
- package/skills/cm-content-factory/landing/docs/content/memory-system.md +38 -0
- package/skills/cm-content-factory/landing/docs/content/openspace.md +27 -0
- package/skills/cm-content-factory/landing/docs/content/use-cases.md +26 -0
- package/skills/cm-content-factory/landing/docs/content/v5-intro.md +28 -0
- package/skills/cm-content-factory/landing/docs/index.html +240 -0
- package/skills/cm-content-factory/landing/index.html +100 -100
- package/skills/cm-content-factory/landing/script.js +42 -0
- package/skills/cm-content-factory/landing/translations.js +400 -400
- package/skills/cm-continuity/SKILL.md +32 -33
- package/skills/cm-design-studio/SKILL.md +34 -0
- package/skills/cm-ecosystem-roadmap/SKILL.md +15 -0
- package/skills/cm-engineering-meta/SKILL.md +73 -0
- package/skills/cm-growth-hacking/SKILL.md +1 -12
- package/skills/cm-guardian-runtime/SKILL.md +26 -0
- package/skills/cm-mcp-engineering/SKILL.md +22 -0
- package/skills/cm-notebooklm/SKILL.md +1 -17
- package/skills/cm-post-deploy-canary/SKILL.md +22 -0
- package/skills/cm-project-bootstrap/SKILL.md +11 -0
- package/skills/cm-qa-visual-cli/SKILL.md +22 -0
- package/skills/cm-retro-cli/SKILL.md +23 -0
- package/skills/cm-second-opinion-cli/SKILL.md +23 -0
- package/skills/cm-secret-shield/SKILL.md +2 -2
- package/skills/cm-security-gate/SKILL.md +1 -0
- package/skills/cm-skill-chain/SKILL.md +25 -4
- package/skills/cm-skill-evolution/SKILL.md +83 -0
- package/skills/cm-skill-health/SKILL.md +83 -0
- package/skills/cm-skill-index/SKILL.md +11 -3
- package/skills/cm-skill-search/SKILL.md +49 -0
- package/skills/cm-skill-share/SKILL.md +58 -0
- package/skills/cm-sprint-bus/SKILL.md +33 -0
- package/skills/cm-start/SKILL.md +0 -10
- package/skills/cm-tdd/SKILL.md +59 -72
- package/skills/profiles/README.md +21 -0
- package/skills/profiles/core.txt +23 -0
- package/skills/profiles/design.txt +6 -0
- package/skills/profiles/full.txt +62 -0
- package/skills/profiles/growth.txt +10 -0
- package/skills/profiles/knowledge.txt +7 -0
- package/install.sh +0 -901
- package/scripts/test-gemini.js +0 -13
- package/skills/cm-frappe-agent/SKILL.md +0 -134
- package/skills/cm-frappe-agent/agents/doctype-architect.md +0 -596
- package/skills/cm-frappe-agent/agents/erpnext-customizer.md +0 -643
- package/skills/cm-frappe-agent/agents/frappe-backend.md +0 -814
- package/skills/cm-frappe-agent/agents/frappe-custom-frontend.md +0 -557
- package/skills/cm-frappe-agent/agents/frappe-debugger.md +0 -625
- package/skills/cm-frappe-agent/agents/frappe-fixer.md +0 -275
- package/skills/cm-frappe-agent/agents/frappe-frontend.md +0 -660
- package/skills/cm-frappe-agent/agents/frappe-installer.md +0 -158
- package/skills/cm-frappe-agent/agents/frappe-performance.md +0 -307
- package/skills/cm-frappe-agent/agents/frappe-planner.md +0 -419
- package/skills/cm-frappe-agent/agents/frappe-remote-ops.md +0 -153
- package/skills/cm-frappe-agent/agents/github-workflow.md +0 -286
- package/skills/cm-frappe-agent/commands/frappe-app.md +0 -351
- package/skills/cm-frappe-agent/commands/frappe-backend.md +0 -162
- package/skills/cm-frappe-agent/commands/frappe-bench.md +0 -254
- package/skills/cm-frappe-agent/commands/frappe-debug.md +0 -263
- package/skills/cm-frappe-agent/commands/frappe-doctype-create.md +0 -272
- package/skills/cm-frappe-agent/commands/frappe-doctype-field.md +0 -310
- package/skills/cm-frappe-agent/commands/frappe-erpnext.md +0 -210
- package/skills/cm-frappe-agent/commands/frappe-fix.md +0 -59
- package/skills/cm-frappe-agent/commands/frappe-frontend.md +0 -210
- package/skills/cm-frappe-agent/commands/frappe-fullstack.md +0 -243
- package/skills/cm-frappe-agent/commands/frappe-github.md +0 -57
- package/skills/cm-frappe-agent/commands/frappe-install.md +0 -52
- package/skills/cm-frappe-agent/commands/frappe-plan.md +0 -442
- package/skills/cm-frappe-agent/commands/frappe-remote.md +0 -58
- package/skills/cm-frappe-agent/commands/frappe-test.md +0 -356
- package/skills/cm-frappe-agent/docs/README.md +0 -51
- package/skills/cm-frappe-agent/docs/agents-catalog.md +0 -113
- package/skills/cm-frappe-agent/docs/architecture.md +0 -149
- package/skills/cm-frappe-agent/docs/commands-catalog.md +0 -82
- package/skills/cm-frappe-agent/docs/resources-catalog.md +0 -66
- package/skills/cm-frappe-agent/docs/sitemap-urls.txt +0 -52
- package/skills/cm-frappe-agent/docs/sitemap.md +0 -81
- package/skills/cm-frappe-agent/docs/sop/user-guide.md +0 -178
- package/skills/cm-frappe-agent/docs/sop/vibe-coding-guide.md +0 -122
- package/skills/cm-frappe-agent/resources/7-layer-architecture.md +0 -985
- package/skills/cm-frappe-agent/resources/bench_commands.md +0 -73
- package/skills/cm-frappe-agent/resources/code-patterns-guide.md +0 -948
- package/skills/cm-frappe-agent/resources/common_pitfalls.md +0 -266
- package/skills/cm-frappe-agent/resources/doctype-registry.md +0 -158
- package/skills/cm-frappe-agent/resources/installation-guide.md +0 -289
- package/skills/cm-frappe-agent/resources/rest-api-patterns.md +0 -182
- package/skills/cm-frappe-agent/resources/scaffold_checklist.md +0 -82
- package/skills/cm-frappe-agent/resources/upgrade_patterns.md +0 -113
- package/skills/cm-frappe-agent/resources/web-form-patterns.md +0 -252
- package/skills/cm-frappe-agent/skills/bench-commands/SKILL.md +0 -621
- package/skills/cm-frappe-agent/skills/client-scripts/SKILL.md +0 -642
- package/skills/cm-frappe-agent/skills/doctype-patterns/SKILL.md +0 -576
- package/skills/cm-frappe-agent/skills/frappe-api/SKILL.md +0 -740
- package/skills/cm-frappe-agent/skills/remote-operations/SKILL.md +0 -47
- package/skills/cm-frappe-agent/skills/server-scripts/SKILL.md +0 -608
- package/skills/cm-frappe-agent/skills/web-forms/SKILL.md +0 -46
- package/skills/frappe-app-builder.zip +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.registerEngineeringCommands = registerEngineeringCommands;
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const http_1 = __importDefault(require("http"));
|
|
20
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
21
|
+
const browse_server_1 = require("../../browse-server");
|
|
22
|
+
const guardian_core_1 = require("../../guardian-core");
|
|
23
|
+
const cm_config_1 = require("../../cm-config");
|
|
24
|
+
const second_opinion_providers_1 = require("../../second-opinion-providers");
|
|
25
|
+
const sprint_pipeline_1 = require("../../sprint-pipeline");
|
|
26
|
+
const retro_summary_1 = require("../../retro-summary");
|
|
27
|
+
const cm_suggest_1 = require("../../cm-suggest");
|
|
28
|
+
const storage_backend_1 = require("../../storage-backend");
|
|
29
|
+
const advisory_report_1 = require("../../advisory-report");
|
|
30
|
+
const advisory_handoff_1 = require("../../advisory-handoff");
|
|
31
|
+
function projectPath(opt) {
|
|
32
|
+
return path_1.default.resolve(opt || process.cwd());
|
|
33
|
+
}
|
|
34
|
+
function registerEngineeringCommands(program) {
|
|
35
|
+
const browse = program.command('browse').description('Playwright browse daemon (local QA / screenshots)');
|
|
36
|
+
browse
|
|
37
|
+
.command('start')
|
|
38
|
+
.option('-p, --port <n>', 'port (default: .cm/config.yaml browse.port or 17395)')
|
|
39
|
+
.option('-H, --host <h>', 'bind host (default: config or 127.0.0.1)')
|
|
40
|
+
.option('--token <t>', 'bearer token (or env CM_BROWSE_TOKEN or config browse.token)')
|
|
41
|
+
.option('--headed', 'headed browser', false)
|
|
42
|
+
.action((opts) => __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
44
|
+
const root = process.cwd();
|
|
45
|
+
const cfg = (0, cm_config_1.loadCmConfig)(root);
|
|
46
|
+
const port = parseInt(String((_c = (_a = opts.port) !== null && _a !== void 0 ? _a : (_b = cfg.browse) === null || _b === void 0 ? void 0 : _b.port) !== null && _c !== void 0 ? _c : 17395), 10);
|
|
47
|
+
const host = String((_f = (_d = opts.host) !== null && _d !== void 0 ? _d : (_e = cfg.browse) === null || _e === void 0 ? void 0 : _e.host) !== null && _f !== void 0 ? _f : '127.0.0.1');
|
|
48
|
+
const token = opts.token ||
|
|
49
|
+
process.env.CM_BROWSE_TOKEN ||
|
|
50
|
+
((_g = cfg.browse) === null || _g === void 0 ? void 0 : _g.token) ||
|
|
51
|
+
'dev-token-change-me';
|
|
52
|
+
const daemon = new browse_server_1.BrowseDaemon({
|
|
53
|
+
host,
|
|
54
|
+
port,
|
|
55
|
+
token,
|
|
56
|
+
headless: !opts.headed,
|
|
57
|
+
});
|
|
58
|
+
yield daemon.listen();
|
|
59
|
+
console.log(chalk_1.default.green(`cm-browse listening http://${host}:${port}`));
|
|
60
|
+
console.log(chalk_1.default.dim(`Authorization: Bearer ${token.slice(0, 8)}…`));
|
|
61
|
+
console.log(chalk_1.default.dim('POST /session/start, /navigate, /refs/refresh, /click, /fill, GET /screenshot'));
|
|
62
|
+
process.on('SIGINT', () => __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
yield daemon.close();
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}));
|
|
66
|
+
}));
|
|
67
|
+
const guardian = program.command('guardian').description('Runtime safety: destructive command patterns + path freeze');
|
|
68
|
+
guardian
|
|
69
|
+
.command('check')
|
|
70
|
+
.argument('<cmd...>', 'shell command to check')
|
|
71
|
+
.action((parts) => {
|
|
72
|
+
var _a;
|
|
73
|
+
const cmd = parts.join(' ');
|
|
74
|
+
const cfg = (0, cm_config_1.loadCmConfig)(process.cwd());
|
|
75
|
+
const extra = (_a = cfg.guardian) === null || _a === void 0 ? void 0 : _a.whitelist_prefixes;
|
|
76
|
+
const r = (0, guardian_core_1.checkShellCommand)(cmd, (extra === null || extra === void 0 ? void 0 : extra.length) ? { extraWhitelist: extra } : undefined);
|
|
77
|
+
if (!r.safe) {
|
|
78
|
+
console.error(chalk_1.default.red('BLOCKED:'), r.reason);
|
|
79
|
+
console.error(chalk_1.default.dim('Pattern:'), r.matchedPattern);
|
|
80
|
+
(0, guardian_core_1.appendGuardianLog)(process.cwd(), `BLOCKED: ${cmd}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
console.log(chalk_1.default.green('OK'), chalk_1.default.dim(cmd));
|
|
84
|
+
});
|
|
85
|
+
guardian
|
|
86
|
+
.command('path-check')
|
|
87
|
+
.requiredOption('--file <f>', 'file path')
|
|
88
|
+
.option('--roots <r>', 'comma-separated roots (default: config guardian.freeze_roots or src,lib)')
|
|
89
|
+
.action((opts) => {
|
|
90
|
+
var _a, _b, _c;
|
|
91
|
+
const cwd = process.cwd();
|
|
92
|
+
const cfg = (0, cm_config_1.loadCmConfig)(cwd);
|
|
93
|
+
const rootsCsv = (_a = opts.roots) !== null && _a !== void 0 ? _a : (((_c = (_b = cfg.guardian) === null || _b === void 0 ? void 0 : _b.freeze_roots) === null || _c === void 0 ? void 0 : _c.length) ? cfg.guardian.freeze_roots.join(',') : 'src,lib');
|
|
94
|
+
const roots = (0, guardian_core_1.normalizeRoots)(cwd, String(rootsCsv)
|
|
95
|
+
.split(',')
|
|
96
|
+
.map((s) => s.trim())
|
|
97
|
+
.filter(Boolean));
|
|
98
|
+
const ok = (0, guardian_core_1.isPathUnderRoots)(opts.file, roots);
|
|
99
|
+
if (!ok) {
|
|
100
|
+
console.error(chalk_1.default.red('Path outside freeze roots:'), opts.file);
|
|
101
|
+
(0, guardian_core_1.appendGuardianLog)(cwd, `FREEZE_VIOLATION: ${opts.file}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
console.log(chalk_1.default.green('OK'), opts.file);
|
|
105
|
+
});
|
|
106
|
+
const advisory = program
|
|
107
|
+
.command('advisory')
|
|
108
|
+
.description('Operator-facing execution analysis and skill quality reports');
|
|
109
|
+
advisory
|
|
110
|
+
.command('report')
|
|
111
|
+
.description('Show recent execution analyses with recommended actions')
|
|
112
|
+
.option('--project <dir>')
|
|
113
|
+
.option('--limit <n>', 'number of analyses to show', '10')
|
|
114
|
+
.action((opts) => {
|
|
115
|
+
var _a;
|
|
116
|
+
const root = projectPath(opts.project);
|
|
117
|
+
const backend = (0, storage_backend_1.getBackend)(root);
|
|
118
|
+
backend.initialize();
|
|
119
|
+
try {
|
|
120
|
+
const limit = Math.max(1, parseInt(String((_a = opts.limit) !== null && _a !== void 0 ? _a : '10'), 10) || 10);
|
|
121
|
+
console.log((0, advisory_report_1.formatAdvisoryReport)(backend, { limit }));
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
backend.close();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
advisory
|
|
128
|
+
.command('metrics')
|
|
129
|
+
.description('Show aggregated skill metrics with quality weights')
|
|
130
|
+
.option('--project <dir>')
|
|
131
|
+
.option('--limit <n>', 'number of skills to show', '10')
|
|
132
|
+
.action((opts) => {
|
|
133
|
+
var _a;
|
|
134
|
+
const root = projectPath(opts.project);
|
|
135
|
+
const backend = (0, storage_backend_1.getBackend)(root);
|
|
136
|
+
backend.initialize();
|
|
137
|
+
try {
|
|
138
|
+
const limit = Math.max(1, parseInt(String((_a = opts.limit) !== null && _a !== void 0 ? _a : '10'), 10) || 10);
|
|
139
|
+
console.log((0, advisory_report_1.formatAdvisoryMetrics)(backend, { limit }));
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
backend.close();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
advisory
|
|
146
|
+
.command('handoff')
|
|
147
|
+
.description('Build a structured advisory handoff for cm-skill-health or cm-skill-evolution')
|
|
148
|
+
.requiredOption('--for <consumer>', 'cm-skill-health | cm-skill-evolution')
|
|
149
|
+
.option('--analysis <id>', 'analysis id prefix (default: latest)')
|
|
150
|
+
.option('--skill <name>', 'override target skill')
|
|
151
|
+
.option('--format <f>', 'md | json', 'md')
|
|
152
|
+
.option('--project <dir>')
|
|
153
|
+
.action((opts) => {
|
|
154
|
+
var _a;
|
|
155
|
+
const consumer = String(opts.for);
|
|
156
|
+
if (consumer !== 'cm-skill-health' && consumer !== 'cm-skill-evolution') {
|
|
157
|
+
console.error(chalk_1.default.red('Invalid --for value. Use cm-skill-health or cm-skill-evolution.'));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
const root = projectPath(opts.project);
|
|
161
|
+
const backend = (0, storage_backend_1.getBackend)(root);
|
|
162
|
+
backend.initialize();
|
|
163
|
+
try {
|
|
164
|
+
const handoff = (0, advisory_handoff_1.buildAdvisoryHandoff)(backend, {
|
|
165
|
+
consumer,
|
|
166
|
+
analysisId: opts.analysis,
|
|
167
|
+
skill: opts.skill,
|
|
168
|
+
});
|
|
169
|
+
const format = String((_a = opts.format) !== null && _a !== void 0 ? _a : 'md').toLowerCase();
|
|
170
|
+
if (format === 'json')
|
|
171
|
+
console.log(JSON.stringify(handoff, null, 2));
|
|
172
|
+
else
|
|
173
|
+
console.log((0, advisory_handoff_1.formatAdvisoryHandoffMarkdown)(handoff));
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error(chalk_1.default.red(error.message));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
backend.close();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
const sprint = program.command('sprint').description('Opinionated pipeline + .cm/sprint Context Bus');
|
|
184
|
+
sprint
|
|
185
|
+
.command('init')
|
|
186
|
+
.option('--project <dir>')
|
|
187
|
+
.option('--from <step>', `one of: ${sprint_pipeline_1.SPRINT_STEPS.join(',')}`)
|
|
188
|
+
.action((opts) => {
|
|
189
|
+
const root = projectPath(opts.project);
|
|
190
|
+
const from = opts.from;
|
|
191
|
+
if (from && !sprint_pipeline_1.SPRINT_STEPS.includes(from)) {
|
|
192
|
+
console.error(chalk_1.default.red('Invalid --from'));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
const state = (0, sprint_pipeline_1.initSprint)(root, from);
|
|
196
|
+
console.log(chalk_1.default.green('Sprint initialized'));
|
|
197
|
+
console.log(chalk_1.default.dim(JSON.stringify(state, null, 2)));
|
|
198
|
+
});
|
|
199
|
+
sprint
|
|
200
|
+
.command('status')
|
|
201
|
+
.option('--project <dir>')
|
|
202
|
+
.action((opts) => {
|
|
203
|
+
const root = projectPath(opts.project);
|
|
204
|
+
const state = (0, sprint_pipeline_1.readSprintState)(root);
|
|
205
|
+
if (!state) {
|
|
206
|
+
console.log(chalk_1.default.yellow('No sprint state. Run: cm sprint init'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const next = state.current_index >= state.pipeline.length
|
|
210
|
+
? '(done)'
|
|
211
|
+
: state.pipeline[state.current_index];
|
|
212
|
+
console.log(chalk_1.default.cyan('Current step:'), next);
|
|
213
|
+
if (typeof next === 'string' && next !== '(done)')
|
|
214
|
+
console.log(chalk_1.default.dim('Skill hint:'), (0, sprint_pipeline_1.skillMappingForStep)(next));
|
|
215
|
+
console.log(chalk_1.default.dim('Completed:'), state.completed.join(', ') || '(none)');
|
|
216
|
+
console.log(chalk_1.default.dim('Skipped:'), state.skipped.join(', ') || '(none)');
|
|
217
|
+
});
|
|
218
|
+
sprint
|
|
219
|
+
.command('complete')
|
|
220
|
+
.argument('<step>', 'step name')
|
|
221
|
+
.option('--project <dir>')
|
|
222
|
+
.option('-m, --message <text>', 'artifact markdown body', '')
|
|
223
|
+
.action((step, opts) => {
|
|
224
|
+
const root = projectPath(opts.project);
|
|
225
|
+
if (!sprint_pipeline_1.SPRINT_STEPS.includes(step)) {
|
|
226
|
+
console.error(chalk_1.default.red('Invalid step'));
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const body = opts.message ||
|
|
230
|
+
`# ${step}\n\n_Completed via \`cm sprint complete\` — replace with real notes._\n`;
|
|
231
|
+
try {
|
|
232
|
+
const state = (0, sprint_pipeline_1.completeSprintStep)(root, step, body);
|
|
233
|
+
console.log(chalk_1.default.green('Step recorded:', step));
|
|
234
|
+
console.log(chalk_1.default.dim('Next index:', state.current_index));
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
console.error(chalk_1.default.red(e.message));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
sprint
|
|
242
|
+
.command('skip')
|
|
243
|
+
.argument('[step]', 'step name (default: current step)')
|
|
244
|
+
.option('--project <dir>')
|
|
245
|
+
.action((step, opts) => {
|
|
246
|
+
const root = projectPath(opts.project);
|
|
247
|
+
const state = (0, sprint_pipeline_1.readSprintState)(root);
|
|
248
|
+
if (!state) {
|
|
249
|
+
console.error(chalk_1.default.red('No sprint state. Run: cm sprint init'));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
if (state.current_index >= state.pipeline.length) {
|
|
253
|
+
console.error(chalk_1.default.red('Sprint pipeline already finished'));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
const current = state.pipeline[state.current_index];
|
|
257
|
+
const target = (step || current);
|
|
258
|
+
if (step && !sprint_pipeline_1.SPRINT_STEPS.includes(target)) {
|
|
259
|
+
console.error(chalk_1.default.red('Invalid step'));
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const next = (0, sprint_pipeline_1.skipSprintStep)(root, target);
|
|
264
|
+
console.log(chalk_1.default.green('Skipped step:', target));
|
|
265
|
+
console.log(chalk_1.default.dim('Next index:', next.current_index));
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
console.error(chalk_1.default.red(e.message));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
sprint
|
|
273
|
+
.command('reset')
|
|
274
|
+
.option('--project <dir>')
|
|
275
|
+
.option('--no-backup', 'do not copy sprint files to .cm/sprint/backup before clearing')
|
|
276
|
+
.action((opts) => {
|
|
277
|
+
const root = projectPath(opts.project);
|
|
278
|
+
const r = (0, sprint_pipeline_1.resetSprint)(root, { backup: opts.backup !== false });
|
|
279
|
+
if (!r.ok) {
|
|
280
|
+
console.log(chalk_1.default.yellow('Nothing to reset (no sprint data under .cm/sprint).'));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (r.backupDir)
|
|
284
|
+
console.log(chalk_1.default.dim('Backup:'), r.backupDir);
|
|
285
|
+
console.log(chalk_1.default.green('Sprint data cleared. Run: cm sprint init'));
|
|
286
|
+
});
|
|
287
|
+
sprint
|
|
288
|
+
.command('dry-run')
|
|
289
|
+
.option('--project <dir>')
|
|
290
|
+
.action((opts) => {
|
|
291
|
+
const root = projectPath(opts.project);
|
|
292
|
+
const d = (0, sprint_pipeline_1.sprintDryRun)(root);
|
|
293
|
+
console.log(chalk_1.default.cyan('Steps:'), d.steps.join(' → '));
|
|
294
|
+
d.artifacts.forEach((a) => console.log(chalk_1.default.dim(' -'), a));
|
|
295
|
+
});
|
|
296
|
+
program
|
|
297
|
+
.command('second-opinion')
|
|
298
|
+
.description('Send unified diff to a secondary model (redacts obvious secrets)')
|
|
299
|
+
.option('--file <f>', 'file containing diff text')
|
|
300
|
+
.option('--provider <p>', 'openai | anthropic', 'openai')
|
|
301
|
+
.action((opts) => __awaiter(this, void 0, void 0, function* () {
|
|
302
|
+
const provider = String(opts.provider || 'openai').toLowerCase();
|
|
303
|
+
const raw = opts.file ? fs_1.default.readFileSync(opts.file, 'utf8') : '';
|
|
304
|
+
const text = raw ? (0, second_opinion_providers_1.redactDiffForReview)(raw) : '';
|
|
305
|
+
if (!opts.file || !text.trim()) {
|
|
306
|
+
console.log(chalk_1.default.yellow('Pass --file <diff.txt>. Set OPENAI_API_KEY (openai) or ANTHROPIC_API_KEY (anthropic).'));
|
|
307
|
+
console.log(chalk_1.default.dim('Diff content is redacted for common secret patterns before sending.'));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
let out;
|
|
312
|
+
if (provider === 'anthropic')
|
|
313
|
+
out = yield (0, second_opinion_providers_1.reviewWithAnthropic)(text);
|
|
314
|
+
else if (provider === 'openai')
|
|
315
|
+
out = yield (0, second_opinion_providers_1.reviewWithOpenAI)(text);
|
|
316
|
+
else {
|
|
317
|
+
console.error(chalk_1.default.red('Unknown --provider (use openai or anthropic)'));
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
console.log(out);
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
console.error(chalk_1.default.red(e.message));
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
}));
|
|
327
|
+
program
|
|
328
|
+
.command('qa-visual')
|
|
329
|
+
.description('Hit cm-browse for screenshot + health (requires browse running)')
|
|
330
|
+
.requiredOption('--url <u>', 'page URL to navigate')
|
|
331
|
+
.option('--port <n>', 'browse daemon port (default: config browse.port or 17395)')
|
|
332
|
+
.option('--token <t>', 'or env CM_BROWSE_TOKEN or config browse.token')
|
|
333
|
+
.action((opts) => __awaiter(this, void 0, void 0, function* () {
|
|
334
|
+
var _a, _b, _c, _d;
|
|
335
|
+
const cfg = (0, cm_config_1.loadCmConfig)(process.cwd());
|
|
336
|
+
const token = opts.token || process.env.CM_BROWSE_TOKEN || ((_a = cfg.browse) === null || _a === void 0 ? void 0 : _a.token) || 'dev-token-change-me';
|
|
337
|
+
const port = parseInt(String((_d = (_b = opts.port) !== null && _b !== void 0 ? _b : (_c = cfg.browse) === null || _c === void 0 ? void 0 : _c.port) !== null && _d !== void 0 ? _d : 17395), 10);
|
|
338
|
+
const auth = `Bearer ${token}`;
|
|
339
|
+
yield browseRequest(port, '/session/start', 'POST', auth, { headless: true });
|
|
340
|
+
yield browseRequest(port, '/navigate', 'POST', auth, { url: opts.url });
|
|
341
|
+
yield browseRequest(port, '/refs/refresh', 'POST', auth, {});
|
|
342
|
+
const png = yield browseBuffer(port, '/screenshot', auth);
|
|
343
|
+
const out = path_1.default.join(process.cwd(), 'cm-qa-visual.png');
|
|
344
|
+
fs_1.default.writeFileSync(out, png);
|
|
345
|
+
console.log(chalk_1.default.green('Screenshot saved'), out);
|
|
346
|
+
}));
|
|
347
|
+
program
|
|
348
|
+
.command('canary')
|
|
349
|
+
.description('Post-deploy smoke: HTTP fetch + optional browse console; baseline compare')
|
|
350
|
+
.requiredOption('--url <u>', 'URL to fetch (http/https)')
|
|
351
|
+
.option('--browse-port <n>', 'if set, GET /console from browse (default: config canary.browse_port)')
|
|
352
|
+
.option('--token <t>', 'browse token (env CM_BROWSE_TOKEN or config)')
|
|
353
|
+
.option('--save-baseline', 'write .cm/canary-baseline.json after check')
|
|
354
|
+
.option('--compare-baseline', 'fail on HTTP regression or 2× latency vs baseline')
|
|
355
|
+
.action((opts) => __awaiter(this, void 0, void 0, function* () {
|
|
356
|
+
var _a, _b, _c, _d;
|
|
357
|
+
const root = process.cwd();
|
|
358
|
+
const cfg = (0, cm_config_1.loadCmConfig)(root);
|
|
359
|
+
const u = new URL(opts.url);
|
|
360
|
+
const { status, latency_ms } = yield httpProbeUrl(u.href);
|
|
361
|
+
if (status >= 400) {
|
|
362
|
+
console.error(chalk_1.default.red(`HTTP ${status}`), u.href);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
if (opts.compareBaseline) {
|
|
366
|
+
const baselinePath = path_1.default.join(root, '.cm', 'canary-baseline.json');
|
|
367
|
+
if (!fs_1.default.existsSync(baselinePath)) {
|
|
368
|
+
console.error(chalk_1.default.red('No baseline file. Run once with --save-baseline'));
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
const prev = JSON.parse(fs_1.default.readFileSync(baselinePath, 'utf8'));
|
|
372
|
+
if (prev.http_status !== undefined &&
|
|
373
|
+
prev.http_status < 400 &&
|
|
374
|
+
status >= 400) {
|
|
375
|
+
console.error(chalk_1.default.red('HTTP regression vs baseline'));
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
if (typeof prev.latency_ms === 'number' &&
|
|
379
|
+
prev.latency_ms > 50 &&
|
|
380
|
+
latency_ms > prev.latency_ms * 2) {
|
|
381
|
+
console.error(chalk_1.default.red(`Latency regression: ${latency_ms}ms vs baseline ${prev.latency_ms}ms`));
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
console.log(chalk_1.default.dim('Baseline compare OK'));
|
|
385
|
+
}
|
|
386
|
+
if (opts.saveBaseline) {
|
|
387
|
+
const baselinePath = path_1.default.join(root, '.cm', 'canary-baseline.json');
|
|
388
|
+
fs_1.default.mkdirSync(path_1.default.dirname(baselinePath), { recursive: true });
|
|
389
|
+
fs_1.default.writeFileSync(baselinePath, JSON.stringify({
|
|
390
|
+
url: opts.url,
|
|
391
|
+
http_status: status,
|
|
392
|
+
latency_ms,
|
|
393
|
+
at: new Date().toISOString(),
|
|
394
|
+
}, null, 2), 'utf8');
|
|
395
|
+
console.log(chalk_1.default.dim('Wrote'), baselinePath);
|
|
396
|
+
}
|
|
397
|
+
console.log(chalk_1.default.green('HTTP OK'), u.href, chalk_1.default.dim(`${status} ${latency_ms}ms`));
|
|
398
|
+
const browsePort = (_a = opts.browsePort) !== null && _a !== void 0 ? _a : (((_b = cfg.canary) === null || _b === void 0 ? void 0 : _b.browse_port) != null ? String(cfg.canary.browse_port) : undefined);
|
|
399
|
+
if (browsePort) {
|
|
400
|
+
const token = opts.token ||
|
|
401
|
+
process.env.CM_BROWSE_TOKEN ||
|
|
402
|
+
((_c = cfg.canary) === null || _c === void 0 ? void 0 : _c.token) ||
|
|
403
|
+
((_d = cfg.browse) === null || _d === void 0 ? void 0 : _d.token) ||
|
|
404
|
+
'dev-token-change-me';
|
|
405
|
+
const raw = yield browseRaw(parseInt(browsePort, 10), '/console', `Bearer ${token}`);
|
|
406
|
+
console.log(chalk_1.default.dim('Browse console (last messages):'), raw.slice(0, 500));
|
|
407
|
+
}
|
|
408
|
+
}));
|
|
409
|
+
const conductor = program.command('conductor').description('Git worktree helpers for parallel sprints');
|
|
410
|
+
conductor
|
|
411
|
+
.command('add')
|
|
412
|
+
.requiredOption('--at <dir>', 'new worktree directory')
|
|
413
|
+
.requiredOption('--branch <b>', 'branch name')
|
|
414
|
+
.option('--base <b>', 'start from branch', 'main')
|
|
415
|
+
.action((opts) => {
|
|
416
|
+
(0, child_process_1.execSync)(`git worktree add -b ${opts.branch} ${opts.at} ${opts.base}`, {
|
|
417
|
+
stdio: 'inherit',
|
|
418
|
+
cwd: process.cwd(),
|
|
419
|
+
});
|
|
420
|
+
console.log(chalk_1.default.green('Worktree created'));
|
|
421
|
+
});
|
|
422
|
+
conductor.command('list').action(() => {
|
|
423
|
+
(0, child_process_1.execSync)('git worktree list', { stdio: 'inherit', cwd: process.cwd() });
|
|
424
|
+
});
|
|
425
|
+
const retro = program
|
|
426
|
+
.command('retro')
|
|
427
|
+
.description('Append operational learning (.cm/operational-learnings.jsonl) or print summary');
|
|
428
|
+
retro
|
|
429
|
+
.command('summary')
|
|
430
|
+
.description('Aggregate JSONL by tool; optional --since filter')
|
|
431
|
+
.option('--project <dir>')
|
|
432
|
+
.option('--since <iso>', 'include entries on or after this ISO timestamp')
|
|
433
|
+
.option('--format <f>', 'json | md', 'md')
|
|
434
|
+
.action((opts) => {
|
|
435
|
+
const root = projectPath(opts.project);
|
|
436
|
+
const j = path_1.default.join(root, '.cm', 'operational-learnings.jsonl');
|
|
437
|
+
let entries = (0, retro_summary_1.loadRetroEntries)(j);
|
|
438
|
+
if (opts.since)
|
|
439
|
+
entries = (0, retro_summary_1.filterSince)(entries, opts.since);
|
|
440
|
+
const byTool = (0, retro_summary_1.countByTool)(entries);
|
|
441
|
+
const fmt = (opts.format || 'md').toLowerCase();
|
|
442
|
+
if (fmt === 'json')
|
|
443
|
+
console.log((0, retro_summary_1.formatRetroJson)(entries, byTool));
|
|
444
|
+
else
|
|
445
|
+
console.log((0, retro_summary_1.formatRetroMarkdown)(entries, byTool));
|
|
446
|
+
});
|
|
447
|
+
retro
|
|
448
|
+
.option('--note <text>', 'append entry')
|
|
449
|
+
.option('--tool <t>', 'tool label', 'cli')
|
|
450
|
+
.option('--project <dir>')
|
|
451
|
+
.option('--summary', 'print last 20 lines (legacy quick view)')
|
|
452
|
+
.action((opts) => {
|
|
453
|
+
const root = projectPath(opts.project);
|
|
454
|
+
const dir = path_1.default.join(root, '.cm');
|
|
455
|
+
if (!fs_1.default.existsSync(dir))
|
|
456
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
457
|
+
const j = path_1.default.join(dir, 'operational-learnings.jsonl');
|
|
458
|
+
if (opts.summary) {
|
|
459
|
+
if (!fs_1.default.existsSync(j)) {
|
|
460
|
+
console.log(chalk_1.default.yellow('No entries yet'));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const lines = fs_1.default.readFileSync(j, 'utf8').trim().split('\n').filter(Boolean).slice(-20);
|
|
464
|
+
for (const line of lines)
|
|
465
|
+
console.log(line);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (!opts.note) {
|
|
469
|
+
console.log(chalk_1.default.yellow('Pass --note "...", --summary, or: cm retro summary'));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const rec = {
|
|
473
|
+
ts: new Date().toISOString(),
|
|
474
|
+
tool: opts.tool,
|
|
475
|
+
note: opts.note,
|
|
476
|
+
};
|
|
477
|
+
fs_1.default.appendFileSync(j, JSON.stringify(rec) + '\n', 'utf8');
|
|
478
|
+
console.log(chalk_1.default.green('Recorded'));
|
|
479
|
+
});
|
|
480
|
+
program
|
|
481
|
+
.command('suggest')
|
|
482
|
+
.description('Proactive skill hints from git status + sprint state')
|
|
483
|
+
.option('--project <dir>')
|
|
484
|
+
.action((opts) => {
|
|
485
|
+
const root = projectPath(opts.project);
|
|
486
|
+
const list = (0, cm_suggest_1.suggestFromContext)(root);
|
|
487
|
+
if (list.length === 0) {
|
|
488
|
+
console.log(chalk_1.default.yellow('No strong signals. Try cm-start or cm-planning for the next step.'));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
for (const s of list) {
|
|
492
|
+
console.log(chalk_1.default.cyan(s.skill));
|
|
493
|
+
console.log(chalk_1.default.dim(` ${s.reason}`));
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
const indexer = program.command('index').description('Project intelligence indexing');
|
|
497
|
+
indexer
|
|
498
|
+
.command('skills')
|
|
499
|
+
.description('Detect tech stack and build .cm/project-skills.md')
|
|
500
|
+
.option('--project <dir>')
|
|
501
|
+
.action((opts) => {
|
|
502
|
+
const root = projectPath(opts.project);
|
|
503
|
+
// Lazy load to avoid module compilation issues at boot if not used
|
|
504
|
+
const { generateProjectSkillsIndex } = require('../../indexer/skills');
|
|
505
|
+
const idx = generateProjectSkillsIndex(root);
|
|
506
|
+
const dotCm = path_1.default.join(root, '.cm');
|
|
507
|
+
if (!fs_1.default.existsSync(dotCm)) {
|
|
508
|
+
fs_1.default.mkdirSync(dotCm, { recursive: true });
|
|
509
|
+
}
|
|
510
|
+
const out = path_1.default.join(dotCm, 'project-skills.md');
|
|
511
|
+
const md = [
|
|
512
|
+
'# Local Project Skills Index',
|
|
513
|
+
'',
|
|
514
|
+
`Detected Technologies: **${idx.detectedTechnologies.join(', ') || 'None'}**`,
|
|
515
|
+
'',
|
|
516
|
+
'## Recommended Community Skills',
|
|
517
|
+
...idx.recommendedSkills.map((s) => `- \`${s}\``),
|
|
518
|
+
'',
|
|
519
|
+
'> Autogenerated by `cm index skills`. Agents should run `npx skills add <skill>` if needed.'
|
|
520
|
+
].join('\n');
|
|
521
|
+
fs_1.default.writeFileSync(out, md, 'utf-8');
|
|
522
|
+
console.log(chalk_1.default.green(`Indexed ${idx.detectedTechnologies.length} technologies and ${idx.recommendedSkills.length} skills to ${out}`));
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
function browseRequest(port, pathname, method, auth, body) {
|
|
526
|
+
return new Promise((resolve, reject) => {
|
|
527
|
+
const data = Buffer.from(JSON.stringify(body));
|
|
528
|
+
const req = http_1.default.request({
|
|
529
|
+
hostname: '127.0.0.1',
|
|
530
|
+
port,
|
|
531
|
+
path: pathname,
|
|
532
|
+
method,
|
|
533
|
+
headers: {
|
|
534
|
+
'Content-Type': 'application/json',
|
|
535
|
+
'Content-Length': data.length,
|
|
536
|
+
Authorization: auth,
|
|
537
|
+
},
|
|
538
|
+
}, (res) => {
|
|
539
|
+
res.resume();
|
|
540
|
+
if (res.statusCode && res.statusCode >= 400)
|
|
541
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
542
|
+
else
|
|
543
|
+
resolve();
|
|
544
|
+
});
|
|
545
|
+
req.on('error', reject);
|
|
546
|
+
req.write(data);
|
|
547
|
+
req.end();
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
function browseBuffer(port, pathname, auth) {
|
|
551
|
+
return new Promise((resolve, reject) => {
|
|
552
|
+
http_1.default.get({
|
|
553
|
+
hostname: '127.0.0.1',
|
|
554
|
+
port,
|
|
555
|
+
path: pathname,
|
|
556
|
+
headers: { Authorization: auth },
|
|
557
|
+
}, (res) => {
|
|
558
|
+
const chunks = [];
|
|
559
|
+
res.on('data', (c) => chunks.push(c));
|
|
560
|
+
res.on('end', () => {
|
|
561
|
+
if (res.statusCode && res.statusCode >= 400)
|
|
562
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
563
|
+
else
|
|
564
|
+
resolve(Buffer.concat(chunks));
|
|
565
|
+
});
|
|
566
|
+
}).on('error', reject);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
function browseRaw(port, pathname, auth) {
|
|
570
|
+
return new Promise((resolve, reject) => {
|
|
571
|
+
http_1.default.get({
|
|
572
|
+
hostname: '127.0.0.1',
|
|
573
|
+
port,
|
|
574
|
+
path: pathname,
|
|
575
|
+
headers: { Authorization: auth },
|
|
576
|
+
}, (res) => {
|
|
577
|
+
let s = '';
|
|
578
|
+
res.on('data', (c) => (s += c));
|
|
579
|
+
res.on('end', () => {
|
|
580
|
+
if (res.statusCode && res.statusCode >= 400)
|
|
581
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
582
|
+
else
|
|
583
|
+
resolve(s);
|
|
584
|
+
});
|
|
585
|
+
}).on('error', reject);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function httpProbeUrl(url) {
|
|
589
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
590
|
+
const t0 = performance.now();
|
|
591
|
+
const res = yield fetch(url, { redirect: 'follow' });
|
|
592
|
+
yield res.arrayBuffer().catch(() => { });
|
|
593
|
+
const latency_ms = Math.round(performance.now() - t0);
|
|
594
|
+
return { status: res.status, latency_ms };
|
|
595
|
+
});
|
|
596
|
+
}
|