codymaster 4.5.4 → 4.8.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 +46 -1
- package/README.md +86 -31
- package/dist/backends/viking-backend.js +235 -0
- package/dist/backends/viking-http-client.js +176 -0
- package/dist/browse-server.js +251 -0
- package/dist/cli/command-registry.js +26 -0
- package/dist/cli/commands/agent.js +120 -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 +488 -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 +110 -0
- package/dist/cm-suggest.js +77 -0
- package/dist/continuity.js +8 -0
- package/dist/distro-validate.js +54 -0
- package/dist/guardian-core.js +74 -0
- package/dist/index.js +36 -2759
- package/dist/mcp-context-server.js +60 -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/sprint-pipeline.js +228 -0
- package/dist/storage-backend.js +63 -0
- package/dist/utils/cli-utils.js +76 -0
- package/dist/utils/skill-utils.js +32 -0
- package/install.sh +286 -58
- package/package.json +16 -5
- package/scripts/build-skills.mjs +51 -0
- package/scripts/gate-0-repo-hygiene.js +75 -0
- package/scripts/postinstall.js +56 -1
- package/scripts/security-scan.js +1 -1
- package/scripts/validate-skills.mjs +42 -0
- package/scripts/viking-demo.ts +105 -0
- package/skills/CLAUDE.md +2 -2
- package/skills/_shared/helpers.md +10 -0
- package/skills/cm-ads-tracker/SKILL.md +3 -6
- package/skills/cm-browse/SKILL.md +28 -0
- package/skills/cm-conductor-worktrees/SKILL.md +24 -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/openspace.md +27 -0
- package/skills/cm-content-factory/landing/docs/content/openviking.md +33 -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 +99 -99
- 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 +33 -6
- package/skills/cm-design-studio/SKILL.md +30 -0
- package/skills/cm-ecosystem-roadmap/SKILL.md +11 -0
- package/skills/cm-engineering-meta/SKILL.md +69 -0
- package/skills/cm-growth-hacking/SKILL.md +1 -12
- package/skills/cm-guardian-runtime/SKILL.md +22 -0
- package/skills/cm-mcp-engineering/SKILL.md +18 -0
- package/skills/cm-notebooklm/SKILL.md +1 -17
- package/skills/cm-post-deploy-canary/SKILL.md +18 -0
- package/skills/cm-qa-visual-cli/SKILL.md +18 -0
- package/skills/cm-retro-cli/SKILL.md +19 -0
- package/skills/cm-second-opinion-cli/SKILL.md +19 -0
- package/skills/cm-secret-shield/SKILL.md +2 -2
- package/skills/cm-sprint-bus/SKILL.md +29 -0
- package/skills/cm-start/SKILL.md +11 -2
- package/skills/cm-tdd/SKILL.md +61 -74
- 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 +58 -0
- package/skills/profiles/growth.txt +10 -0
- package/skills/profiles/knowledge.txt +7 -0
- 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,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.initDesignStudioArtifacts = initDesignStudioArtifacts;
|
|
7
|
+
exports.registerDesignStudioCommands = registerDesignStudioCommands;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
function projectPath(opt) {
|
|
12
|
+
return path_1.default.resolve(opt || process.cwd());
|
|
13
|
+
}
|
|
14
|
+
const CHECKLIST = `# Design studio — checklist
|
|
15
|
+
|
|
16
|
+
- [ ] Problem / JTBD one-liner
|
|
17
|
+
- [ ] 2–3 UI variants named (A/B/C)
|
|
18
|
+
- [ ] Chosen variant + rationale
|
|
19
|
+
- [ ] Handoff block filled in HANDOFF.md
|
|
20
|
+
`;
|
|
21
|
+
const VARIANTS = `# Variants
|
|
22
|
+
|
|
23
|
+
| Id | Name | Notes |
|
|
24
|
+
|----|------|-------|
|
|
25
|
+
| A | | |
|
|
26
|
+
| B | | |
|
|
27
|
+
| C | | |
|
|
28
|
+
`;
|
|
29
|
+
const HANDOFF = `# Handoff to implementation
|
|
30
|
+
|
|
31
|
+
**Chosen variant:** (A/B/C)
|
|
32
|
+
|
|
33
|
+
**Screens / flows:**
|
|
34
|
+
|
|
35
|
+
**Tokens / components to reuse:**
|
|
36
|
+
|
|
37
|
+
**Out of scope:**
|
|
38
|
+
|
|
39
|
+
**Agent prompt stub:**
|
|
40
|
+
|
|
41
|
+
\`\`\`
|
|
42
|
+
Implement the chosen variant using existing design system tokens. …
|
|
43
|
+
\`\`\`
|
|
44
|
+
`;
|
|
45
|
+
const README = `# .cm/design-studio
|
|
46
|
+
|
|
47
|
+
Local artifact folder for **cm-design-studio**: variants, checklist, handoff.
|
|
48
|
+
|
|
49
|
+
Happy path:
|
|
50
|
+
|
|
51
|
+
1. \`cm design-studio init\`
|
|
52
|
+
2. Edit CHECKLIST.md + VARIANTS.md
|
|
53
|
+
3. Fill HANDOFF.md, then run your build skill (e.g. cm-execution) with that stub.
|
|
54
|
+
`;
|
|
55
|
+
/** Writes default artifact files; skips paths that already exist. Returns created + skipped counts. */
|
|
56
|
+
function initDesignStudioArtifacts(root) {
|
|
57
|
+
const base = path_1.default.join(root, '.cm', 'design-studio');
|
|
58
|
+
fs_1.default.mkdirSync(base, { recursive: true });
|
|
59
|
+
const files = [
|
|
60
|
+
['README.md', README],
|
|
61
|
+
['CHECKLIST.md', CHECKLIST],
|
|
62
|
+
['VARIANTS.md', VARIANTS],
|
|
63
|
+
['HANDOFF.md', HANDOFF],
|
|
64
|
+
];
|
|
65
|
+
let created = 0;
|
|
66
|
+
let skipped = 0;
|
|
67
|
+
for (const [name, body] of files) {
|
|
68
|
+
const p = path_1.default.join(base, name);
|
|
69
|
+
if (fs_1.default.existsSync(p)) {
|
|
70
|
+
skipped++;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
fs_1.default.writeFileSync(p, body, 'utf8');
|
|
74
|
+
created++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { created, skipped };
|
|
78
|
+
}
|
|
79
|
+
function registerDesignStudioCommands(program) {
|
|
80
|
+
const ds = program
|
|
81
|
+
.command('design-studio')
|
|
82
|
+
.description('Design variant workspace under .cm/design-studio');
|
|
83
|
+
ds.command('init')
|
|
84
|
+
.description('Create .cm/design-studio with checklist and handoff templates')
|
|
85
|
+
.option('--project <dir>', 'project root', process.cwd())
|
|
86
|
+
.action((opts) => {
|
|
87
|
+
const root = projectPath(opts.project);
|
|
88
|
+
const { created, skipped } = initDesignStudioArtifacts(root);
|
|
89
|
+
const base = path_1.default.join(root, '.cm', 'design-studio');
|
|
90
|
+
if (created)
|
|
91
|
+
console.log(chalk_1.default.green(`wrote ${created} file(s) under`), base);
|
|
92
|
+
if (skipped)
|
|
93
|
+
console.log(chalk_1.default.yellow(`skipped ${skipped} existing`));
|
|
94
|
+
});
|
|
95
|
+
ds.command('status')
|
|
96
|
+
.description('List design-studio artifact files if present')
|
|
97
|
+
.option('--project <dir>', 'project root', process.cwd())
|
|
98
|
+
.action((opts) => {
|
|
99
|
+
const root = projectPath(opts.project);
|
|
100
|
+
const base = path_1.default.join(root, '.cm', 'design-studio');
|
|
101
|
+
if (!fs_1.default.existsSync(base)) {
|
|
102
|
+
console.log(chalk_1.default.yellow('Not initialized. Run:'), chalk_1.default.cyan('cm design-studio init'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const f of fs_1.default.readdirSync(base)) {
|
|
106
|
+
const p = path_1.default.join(base, f);
|
|
107
|
+
const st = fs_1.default.statSync(p);
|
|
108
|
+
console.log(st.isDirectory() ? `${f}/` : f);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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.registerDistroCommands = registerDistroCommands;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const distro_validate_1 = require("../../distro-validate");
|
|
9
|
+
function registerDistroCommands(program) {
|
|
10
|
+
const distro = program.command('distro').description('Skill pack validation (ecosystem roadmap)');
|
|
11
|
+
distro
|
|
12
|
+
.command('validate')
|
|
13
|
+
.description('Validate a skill directory (SKILL.md / tmpl + optional meta.json)')
|
|
14
|
+
.argument('<dir>', 'path to skill folder')
|
|
15
|
+
.action((dir) => {
|
|
16
|
+
const r = (0, distro_validate_1.validateSkillPackDir)(dir);
|
|
17
|
+
for (const w of r.warnings)
|
|
18
|
+
console.log(chalk_1.default.yellow('warning:'), w);
|
|
19
|
+
for (const e of r.errors)
|
|
20
|
+
console.error(chalk_1.default.red('error:'), e);
|
|
21
|
+
if (!r.ok)
|
|
22
|
+
process.exit(1);
|
|
23
|
+
console.log(chalk_1.default.green('OK'), dir);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,488 @@
|
|
|
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
|
+
function projectPath(opt) {
|
|
29
|
+
return path_1.default.resolve(opt || process.cwd());
|
|
30
|
+
}
|
|
31
|
+
function registerEngineeringCommands(program) {
|
|
32
|
+
const browse = program.command('browse').description('Playwright browse daemon (local QA / screenshots)');
|
|
33
|
+
browse
|
|
34
|
+
.command('start')
|
|
35
|
+
.option('-p, --port <n>', 'port (default: .cm/config.yaml browse.port or 17395)')
|
|
36
|
+
.option('-H, --host <h>', 'bind host (default: config or 127.0.0.1)')
|
|
37
|
+
.option('--token <t>', 'bearer token (or env CM_BROWSE_TOKEN or config browse.token)')
|
|
38
|
+
.option('--headed', 'headed browser', false)
|
|
39
|
+
.action((opts) => __awaiter(this, void 0, void 0, function* () {
|
|
40
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
41
|
+
const root = process.cwd();
|
|
42
|
+
const cfg = (0, cm_config_1.loadCmConfig)(root);
|
|
43
|
+
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);
|
|
44
|
+
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');
|
|
45
|
+
const token = opts.token ||
|
|
46
|
+
process.env.CM_BROWSE_TOKEN ||
|
|
47
|
+
((_g = cfg.browse) === null || _g === void 0 ? void 0 : _g.token) ||
|
|
48
|
+
'dev-token-change-me';
|
|
49
|
+
const daemon = new browse_server_1.BrowseDaemon({
|
|
50
|
+
host,
|
|
51
|
+
port,
|
|
52
|
+
token,
|
|
53
|
+
headless: !opts.headed,
|
|
54
|
+
});
|
|
55
|
+
yield daemon.listen();
|
|
56
|
+
console.log(chalk_1.default.green(`cm-browse listening http://${host}:${port}`));
|
|
57
|
+
console.log(chalk_1.default.dim(`Authorization: Bearer ${token.slice(0, 8)}…`));
|
|
58
|
+
console.log(chalk_1.default.dim('POST /session/start, /navigate, /refs/refresh, /click, /fill, GET /screenshot'));
|
|
59
|
+
process.on('SIGINT', () => __awaiter(this, void 0, void 0, function* () {
|
|
60
|
+
yield daemon.close();
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}));
|
|
63
|
+
}));
|
|
64
|
+
const guardian = program.command('guardian').description('Runtime safety: destructive command patterns + path freeze');
|
|
65
|
+
guardian
|
|
66
|
+
.command('check')
|
|
67
|
+
.argument('<cmd...>', 'shell command to check')
|
|
68
|
+
.action((parts) => {
|
|
69
|
+
var _a;
|
|
70
|
+
const cmd = parts.join(' ');
|
|
71
|
+
const cfg = (0, cm_config_1.loadCmConfig)(process.cwd());
|
|
72
|
+
const extra = (_a = cfg.guardian) === null || _a === void 0 ? void 0 : _a.whitelist_prefixes;
|
|
73
|
+
const r = (0, guardian_core_1.checkShellCommand)(cmd, (extra === null || extra === void 0 ? void 0 : extra.length) ? { extraWhitelist: extra } : undefined);
|
|
74
|
+
if (!r.safe) {
|
|
75
|
+
console.error(chalk_1.default.red('BLOCKED:'), r.reason);
|
|
76
|
+
console.error(chalk_1.default.dim('Pattern:'), r.matchedPattern);
|
|
77
|
+
(0, guardian_core_1.appendGuardianLog)(process.cwd(), `BLOCKED: ${cmd}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
console.log(chalk_1.default.green('OK'), chalk_1.default.dim(cmd));
|
|
81
|
+
});
|
|
82
|
+
guardian
|
|
83
|
+
.command('path-check')
|
|
84
|
+
.requiredOption('--file <f>', 'file path')
|
|
85
|
+
.option('--roots <r>', 'comma-separated roots (default: config guardian.freeze_roots or src,lib)')
|
|
86
|
+
.action((opts) => {
|
|
87
|
+
var _a, _b, _c;
|
|
88
|
+
const cwd = process.cwd();
|
|
89
|
+
const cfg = (0, cm_config_1.loadCmConfig)(cwd);
|
|
90
|
+
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');
|
|
91
|
+
const roots = (0, guardian_core_1.normalizeRoots)(cwd, String(rootsCsv)
|
|
92
|
+
.split(',')
|
|
93
|
+
.map((s) => s.trim())
|
|
94
|
+
.filter(Boolean));
|
|
95
|
+
const ok = (0, guardian_core_1.isPathUnderRoots)(opts.file, roots);
|
|
96
|
+
if (!ok) {
|
|
97
|
+
console.error(chalk_1.default.red('Path outside freeze roots:'), opts.file);
|
|
98
|
+
(0, guardian_core_1.appendGuardianLog)(cwd, `FREEZE_VIOLATION: ${opts.file}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
console.log(chalk_1.default.green('OK'), opts.file);
|
|
102
|
+
});
|
|
103
|
+
const sprint = program.command('sprint').description('Opinionated pipeline + .cm/sprint Context Bus');
|
|
104
|
+
sprint
|
|
105
|
+
.command('init')
|
|
106
|
+
.option('--project <dir>')
|
|
107
|
+
.option('--from <step>', `one of: ${sprint_pipeline_1.SPRINT_STEPS.join(',')}`)
|
|
108
|
+
.action((opts) => {
|
|
109
|
+
const root = projectPath(opts.project);
|
|
110
|
+
const from = opts.from;
|
|
111
|
+
if (from && !sprint_pipeline_1.SPRINT_STEPS.includes(from)) {
|
|
112
|
+
console.error(chalk_1.default.red('Invalid --from'));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const state = (0, sprint_pipeline_1.initSprint)(root, from);
|
|
116
|
+
console.log(chalk_1.default.green('Sprint initialized'));
|
|
117
|
+
console.log(chalk_1.default.dim(JSON.stringify(state, null, 2)));
|
|
118
|
+
});
|
|
119
|
+
sprint
|
|
120
|
+
.command('status')
|
|
121
|
+
.option('--project <dir>')
|
|
122
|
+
.action((opts) => {
|
|
123
|
+
const root = projectPath(opts.project);
|
|
124
|
+
const state = (0, sprint_pipeline_1.readSprintState)(root);
|
|
125
|
+
if (!state) {
|
|
126
|
+
console.log(chalk_1.default.yellow('No sprint state. Run: cm sprint init'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const next = state.current_index >= state.pipeline.length
|
|
130
|
+
? '(done)'
|
|
131
|
+
: state.pipeline[state.current_index];
|
|
132
|
+
console.log(chalk_1.default.cyan('Current step:'), next);
|
|
133
|
+
if (typeof next === 'string' && next !== '(done)')
|
|
134
|
+
console.log(chalk_1.default.dim('Skill hint:'), (0, sprint_pipeline_1.skillMappingForStep)(next));
|
|
135
|
+
console.log(chalk_1.default.dim('Completed:'), state.completed.join(', ') || '(none)');
|
|
136
|
+
console.log(chalk_1.default.dim('Skipped:'), state.skipped.join(', ') || '(none)');
|
|
137
|
+
});
|
|
138
|
+
sprint
|
|
139
|
+
.command('complete')
|
|
140
|
+
.argument('<step>', 'step name')
|
|
141
|
+
.option('--project <dir>')
|
|
142
|
+
.option('-m, --message <text>', 'artifact markdown body', '')
|
|
143
|
+
.action((step, opts) => {
|
|
144
|
+
const root = projectPath(opts.project);
|
|
145
|
+
if (!sprint_pipeline_1.SPRINT_STEPS.includes(step)) {
|
|
146
|
+
console.error(chalk_1.default.red('Invalid step'));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
const body = opts.message ||
|
|
150
|
+
`# ${step}\n\n_Completed via \`cm sprint complete\` — replace with real notes._\n`;
|
|
151
|
+
try {
|
|
152
|
+
const state = (0, sprint_pipeline_1.completeSprintStep)(root, step, body);
|
|
153
|
+
console.log(chalk_1.default.green('Step recorded:', step));
|
|
154
|
+
console.log(chalk_1.default.dim('Next index:', state.current_index));
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
console.error(chalk_1.default.red(e.message));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
sprint
|
|
162
|
+
.command('skip')
|
|
163
|
+
.argument('[step]', 'step name (default: current step)')
|
|
164
|
+
.option('--project <dir>')
|
|
165
|
+
.action((step, opts) => {
|
|
166
|
+
const root = projectPath(opts.project);
|
|
167
|
+
const state = (0, sprint_pipeline_1.readSprintState)(root);
|
|
168
|
+
if (!state) {
|
|
169
|
+
console.error(chalk_1.default.red('No sprint state. Run: cm sprint init'));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
if (state.current_index >= state.pipeline.length) {
|
|
173
|
+
console.error(chalk_1.default.red('Sprint pipeline already finished'));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
const current = state.pipeline[state.current_index];
|
|
177
|
+
const target = (step || current);
|
|
178
|
+
if (step && !sprint_pipeline_1.SPRINT_STEPS.includes(target)) {
|
|
179
|
+
console.error(chalk_1.default.red('Invalid step'));
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const next = (0, sprint_pipeline_1.skipSprintStep)(root, target);
|
|
184
|
+
console.log(chalk_1.default.green('Skipped step:', target));
|
|
185
|
+
console.log(chalk_1.default.dim('Next index:', next.current_index));
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
console.error(chalk_1.default.red(e.message));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
sprint
|
|
193
|
+
.command('reset')
|
|
194
|
+
.option('--project <dir>')
|
|
195
|
+
.option('--no-backup', 'do not copy sprint files to .cm/sprint/backup before clearing')
|
|
196
|
+
.action((opts) => {
|
|
197
|
+
const root = projectPath(opts.project);
|
|
198
|
+
const r = (0, sprint_pipeline_1.resetSprint)(root, { backup: opts.backup !== false });
|
|
199
|
+
if (!r.ok) {
|
|
200
|
+
console.log(chalk_1.default.yellow('Nothing to reset (no sprint data under .cm/sprint).'));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (r.backupDir)
|
|
204
|
+
console.log(chalk_1.default.dim('Backup:'), r.backupDir);
|
|
205
|
+
console.log(chalk_1.default.green('Sprint data cleared. Run: cm sprint init'));
|
|
206
|
+
});
|
|
207
|
+
sprint
|
|
208
|
+
.command('dry-run')
|
|
209
|
+
.option('--project <dir>')
|
|
210
|
+
.action((opts) => {
|
|
211
|
+
const root = projectPath(opts.project);
|
|
212
|
+
const d = (0, sprint_pipeline_1.sprintDryRun)(root);
|
|
213
|
+
console.log(chalk_1.default.cyan('Steps:'), d.steps.join(' → '));
|
|
214
|
+
d.artifacts.forEach((a) => console.log(chalk_1.default.dim(' -'), a));
|
|
215
|
+
});
|
|
216
|
+
program
|
|
217
|
+
.command('second-opinion')
|
|
218
|
+
.description('Send unified diff to a secondary model (redacts obvious secrets)')
|
|
219
|
+
.option('--file <f>', 'file containing diff text')
|
|
220
|
+
.option('--provider <p>', 'openai | anthropic', 'openai')
|
|
221
|
+
.action((opts) => __awaiter(this, void 0, void 0, function* () {
|
|
222
|
+
const provider = String(opts.provider || 'openai').toLowerCase();
|
|
223
|
+
const raw = opts.file ? fs_1.default.readFileSync(opts.file, 'utf8') : '';
|
|
224
|
+
const text = raw ? (0, second_opinion_providers_1.redactDiffForReview)(raw) : '';
|
|
225
|
+
if (!opts.file || !text.trim()) {
|
|
226
|
+
console.log(chalk_1.default.yellow('Pass --file <diff.txt>. Set OPENAI_API_KEY (openai) or ANTHROPIC_API_KEY (anthropic).'));
|
|
227
|
+
console.log(chalk_1.default.dim('Diff content is redacted for common secret patterns before sending.'));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
let out;
|
|
232
|
+
if (provider === 'anthropic')
|
|
233
|
+
out = yield (0, second_opinion_providers_1.reviewWithAnthropic)(text);
|
|
234
|
+
else if (provider === 'openai')
|
|
235
|
+
out = yield (0, second_opinion_providers_1.reviewWithOpenAI)(text);
|
|
236
|
+
else {
|
|
237
|
+
console.error(chalk_1.default.red('Unknown --provider (use openai or anthropic)'));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
console.log(out);
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
console.error(chalk_1.default.red(e.message));
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
}));
|
|
247
|
+
program
|
|
248
|
+
.command('qa-visual')
|
|
249
|
+
.description('Hit cm-browse for screenshot + health (requires browse running)')
|
|
250
|
+
.requiredOption('--url <u>', 'page URL to navigate')
|
|
251
|
+
.option('--port <n>', 'browse daemon port (default: config browse.port or 17395)')
|
|
252
|
+
.option('--token <t>', 'or env CM_BROWSE_TOKEN or config browse.token')
|
|
253
|
+
.action((opts) => __awaiter(this, void 0, void 0, function* () {
|
|
254
|
+
var _a, _b, _c, _d;
|
|
255
|
+
const cfg = (0, cm_config_1.loadCmConfig)(process.cwd());
|
|
256
|
+
const token = opts.token || process.env.CM_BROWSE_TOKEN || ((_a = cfg.browse) === null || _a === void 0 ? void 0 : _a.token) || 'dev-token-change-me';
|
|
257
|
+
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);
|
|
258
|
+
const auth = `Bearer ${token}`;
|
|
259
|
+
yield browseRequest(port, '/session/start', 'POST', auth, { headless: true });
|
|
260
|
+
yield browseRequest(port, '/navigate', 'POST', auth, { url: opts.url });
|
|
261
|
+
yield browseRequest(port, '/refs/refresh', 'POST', auth, {});
|
|
262
|
+
const png = yield browseBuffer(port, '/screenshot', auth);
|
|
263
|
+
const out = path_1.default.join(process.cwd(), 'cm-qa-visual.png');
|
|
264
|
+
fs_1.default.writeFileSync(out, png);
|
|
265
|
+
console.log(chalk_1.default.green('Screenshot saved'), out);
|
|
266
|
+
}));
|
|
267
|
+
program
|
|
268
|
+
.command('canary')
|
|
269
|
+
.description('Post-deploy smoke: HTTP fetch + optional browse console; baseline compare')
|
|
270
|
+
.requiredOption('--url <u>', 'URL to fetch (http/https)')
|
|
271
|
+
.option('--browse-port <n>', 'if set, GET /console from browse (default: config canary.browse_port)')
|
|
272
|
+
.option('--token <t>', 'browse token (env CM_BROWSE_TOKEN or config)')
|
|
273
|
+
.option('--save-baseline', 'write .cm/canary-baseline.json after check')
|
|
274
|
+
.option('--compare-baseline', 'fail on HTTP regression or 2× latency vs baseline')
|
|
275
|
+
.action((opts) => __awaiter(this, void 0, void 0, function* () {
|
|
276
|
+
var _a, _b, _c, _d;
|
|
277
|
+
const root = process.cwd();
|
|
278
|
+
const cfg = (0, cm_config_1.loadCmConfig)(root);
|
|
279
|
+
const u = new URL(opts.url);
|
|
280
|
+
const { status, latency_ms } = yield httpProbeUrl(u.href);
|
|
281
|
+
if (status >= 400) {
|
|
282
|
+
console.error(chalk_1.default.red(`HTTP ${status}`), u.href);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
if (opts.compareBaseline) {
|
|
286
|
+
const baselinePath = path_1.default.join(root, '.cm', 'canary-baseline.json');
|
|
287
|
+
if (!fs_1.default.existsSync(baselinePath)) {
|
|
288
|
+
console.error(chalk_1.default.red('No baseline file. Run once with --save-baseline'));
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
const prev = JSON.parse(fs_1.default.readFileSync(baselinePath, 'utf8'));
|
|
292
|
+
if (prev.http_status !== undefined &&
|
|
293
|
+
prev.http_status < 400 &&
|
|
294
|
+
status >= 400) {
|
|
295
|
+
console.error(chalk_1.default.red('HTTP regression vs baseline'));
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
if (typeof prev.latency_ms === 'number' &&
|
|
299
|
+
prev.latency_ms > 50 &&
|
|
300
|
+
latency_ms > prev.latency_ms * 2) {
|
|
301
|
+
console.error(chalk_1.default.red(`Latency regression: ${latency_ms}ms vs baseline ${prev.latency_ms}ms`));
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
console.log(chalk_1.default.dim('Baseline compare OK'));
|
|
305
|
+
}
|
|
306
|
+
if (opts.saveBaseline) {
|
|
307
|
+
const baselinePath = path_1.default.join(root, '.cm', 'canary-baseline.json');
|
|
308
|
+
fs_1.default.mkdirSync(path_1.default.dirname(baselinePath), { recursive: true });
|
|
309
|
+
fs_1.default.writeFileSync(baselinePath, JSON.stringify({
|
|
310
|
+
url: opts.url,
|
|
311
|
+
http_status: status,
|
|
312
|
+
latency_ms,
|
|
313
|
+
at: new Date().toISOString(),
|
|
314
|
+
}, null, 2), 'utf8');
|
|
315
|
+
console.log(chalk_1.default.dim('Wrote'), baselinePath);
|
|
316
|
+
}
|
|
317
|
+
console.log(chalk_1.default.green('HTTP OK'), u.href, chalk_1.default.dim(`${status} ${latency_ms}ms`));
|
|
318
|
+
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);
|
|
319
|
+
if (browsePort) {
|
|
320
|
+
const token = opts.token ||
|
|
321
|
+
process.env.CM_BROWSE_TOKEN ||
|
|
322
|
+
((_c = cfg.canary) === null || _c === void 0 ? void 0 : _c.token) ||
|
|
323
|
+
((_d = cfg.browse) === null || _d === void 0 ? void 0 : _d.token) ||
|
|
324
|
+
'dev-token-change-me';
|
|
325
|
+
const raw = yield browseRaw(parseInt(browsePort, 10), '/console', `Bearer ${token}`);
|
|
326
|
+
console.log(chalk_1.default.dim('Browse console (last messages):'), raw.slice(0, 500));
|
|
327
|
+
}
|
|
328
|
+
}));
|
|
329
|
+
const conductor = program.command('conductor').description('Git worktree helpers for parallel sprints');
|
|
330
|
+
conductor
|
|
331
|
+
.command('add')
|
|
332
|
+
.requiredOption('--at <dir>', 'new worktree directory')
|
|
333
|
+
.requiredOption('--branch <b>', 'branch name')
|
|
334
|
+
.option('--base <b>', 'start from branch', 'main')
|
|
335
|
+
.action((opts) => {
|
|
336
|
+
(0, child_process_1.execSync)(`git worktree add -b ${opts.branch} ${opts.at} ${opts.base}`, {
|
|
337
|
+
stdio: 'inherit',
|
|
338
|
+
cwd: process.cwd(),
|
|
339
|
+
});
|
|
340
|
+
console.log(chalk_1.default.green('Worktree created'));
|
|
341
|
+
});
|
|
342
|
+
conductor.command('list').action(() => {
|
|
343
|
+
(0, child_process_1.execSync)('git worktree list', { stdio: 'inherit', cwd: process.cwd() });
|
|
344
|
+
});
|
|
345
|
+
const retro = program
|
|
346
|
+
.command('retro')
|
|
347
|
+
.description('Append operational learning (.cm/operational-learnings.jsonl) or print summary');
|
|
348
|
+
retro
|
|
349
|
+
.command('summary')
|
|
350
|
+
.description('Aggregate JSONL by tool; optional --since filter')
|
|
351
|
+
.option('--project <dir>')
|
|
352
|
+
.option('--since <iso>', 'include entries on or after this ISO timestamp')
|
|
353
|
+
.option('--format <f>', 'json | md', 'md')
|
|
354
|
+
.action((opts) => {
|
|
355
|
+
const root = projectPath(opts.project);
|
|
356
|
+
const j = path_1.default.join(root, '.cm', 'operational-learnings.jsonl');
|
|
357
|
+
let entries = (0, retro_summary_1.loadRetroEntries)(j);
|
|
358
|
+
if (opts.since)
|
|
359
|
+
entries = (0, retro_summary_1.filterSince)(entries, opts.since);
|
|
360
|
+
const byTool = (0, retro_summary_1.countByTool)(entries);
|
|
361
|
+
const fmt = (opts.format || 'md').toLowerCase();
|
|
362
|
+
if (fmt === 'json')
|
|
363
|
+
console.log((0, retro_summary_1.formatRetroJson)(entries, byTool));
|
|
364
|
+
else
|
|
365
|
+
console.log((0, retro_summary_1.formatRetroMarkdown)(entries, byTool));
|
|
366
|
+
});
|
|
367
|
+
retro
|
|
368
|
+
.option('--note <text>', 'append entry')
|
|
369
|
+
.option('--tool <t>', 'tool label', 'cli')
|
|
370
|
+
.option('--project <dir>')
|
|
371
|
+
.option('--summary', 'print last 20 lines (legacy quick view)')
|
|
372
|
+
.action((opts) => {
|
|
373
|
+
const root = projectPath(opts.project);
|
|
374
|
+
const dir = path_1.default.join(root, '.cm');
|
|
375
|
+
if (!fs_1.default.existsSync(dir))
|
|
376
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
377
|
+
const j = path_1.default.join(dir, 'operational-learnings.jsonl');
|
|
378
|
+
if (opts.summary) {
|
|
379
|
+
if (!fs_1.default.existsSync(j)) {
|
|
380
|
+
console.log(chalk_1.default.yellow('No entries yet'));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const lines = fs_1.default.readFileSync(j, 'utf8').trim().split('\n').filter(Boolean).slice(-20);
|
|
384
|
+
for (const line of lines)
|
|
385
|
+
console.log(line);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (!opts.note) {
|
|
389
|
+
console.log(chalk_1.default.yellow('Pass --note "...", --summary, or: cm retro summary'));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const rec = {
|
|
393
|
+
ts: new Date().toISOString(),
|
|
394
|
+
tool: opts.tool,
|
|
395
|
+
note: opts.note,
|
|
396
|
+
};
|
|
397
|
+
fs_1.default.appendFileSync(j, JSON.stringify(rec) + '\n', 'utf8');
|
|
398
|
+
console.log(chalk_1.default.green('Recorded'));
|
|
399
|
+
});
|
|
400
|
+
program
|
|
401
|
+
.command('suggest')
|
|
402
|
+
.description('Proactive skill hints from git status + sprint state')
|
|
403
|
+
.option('--project <dir>')
|
|
404
|
+
.action((opts) => {
|
|
405
|
+
const root = projectPath(opts.project);
|
|
406
|
+
const list = (0, cm_suggest_1.suggestFromContext)(root);
|
|
407
|
+
if (list.length === 0) {
|
|
408
|
+
console.log(chalk_1.default.yellow('No strong signals. Try cm-start or cm-planning for the next step.'));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
for (const s of list) {
|
|
412
|
+
console.log(chalk_1.default.cyan(s.skill));
|
|
413
|
+
console.log(chalk_1.default.dim(` ${s.reason}`));
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function browseRequest(port, pathname, method, auth, body) {
|
|
418
|
+
return new Promise((resolve, reject) => {
|
|
419
|
+
const data = Buffer.from(JSON.stringify(body));
|
|
420
|
+
const req = http_1.default.request({
|
|
421
|
+
hostname: '127.0.0.1',
|
|
422
|
+
port,
|
|
423
|
+
path: pathname,
|
|
424
|
+
method,
|
|
425
|
+
headers: {
|
|
426
|
+
'Content-Type': 'application/json',
|
|
427
|
+
'Content-Length': data.length,
|
|
428
|
+
Authorization: auth,
|
|
429
|
+
},
|
|
430
|
+
}, (res) => {
|
|
431
|
+
res.resume();
|
|
432
|
+
if (res.statusCode && res.statusCode >= 400)
|
|
433
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
434
|
+
else
|
|
435
|
+
resolve();
|
|
436
|
+
});
|
|
437
|
+
req.on('error', reject);
|
|
438
|
+
req.write(data);
|
|
439
|
+
req.end();
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function browseBuffer(port, pathname, auth) {
|
|
443
|
+
return new Promise((resolve, reject) => {
|
|
444
|
+
http_1.default.get({
|
|
445
|
+
hostname: '127.0.0.1',
|
|
446
|
+
port,
|
|
447
|
+
path: pathname,
|
|
448
|
+
headers: { Authorization: auth },
|
|
449
|
+
}, (res) => {
|
|
450
|
+
const chunks = [];
|
|
451
|
+
res.on('data', (c) => chunks.push(c));
|
|
452
|
+
res.on('end', () => {
|
|
453
|
+
if (res.statusCode && res.statusCode >= 400)
|
|
454
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
455
|
+
else
|
|
456
|
+
resolve(Buffer.concat(chunks));
|
|
457
|
+
});
|
|
458
|
+
}).on('error', reject);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function browseRaw(port, pathname, auth) {
|
|
462
|
+
return new Promise((resolve, reject) => {
|
|
463
|
+
http_1.default.get({
|
|
464
|
+
hostname: '127.0.0.1',
|
|
465
|
+
port,
|
|
466
|
+
path: pathname,
|
|
467
|
+
headers: { Authorization: auth },
|
|
468
|
+
}, (res) => {
|
|
469
|
+
let s = '';
|
|
470
|
+
res.on('data', (c) => (s += c));
|
|
471
|
+
res.on('end', () => {
|
|
472
|
+
if (res.statusCode && res.statusCode >= 400)
|
|
473
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
474
|
+
else
|
|
475
|
+
resolve(s);
|
|
476
|
+
});
|
|
477
|
+
}).on('error', reject);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function httpProbeUrl(url) {
|
|
481
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
482
|
+
const t0 = performance.now();
|
|
483
|
+
const res = yield fetch(url, { redirect: 'follow' });
|
|
484
|
+
yield res.arrayBuffer().catch(() => { });
|
|
485
|
+
const latency_ms = Math.round(performance.now() - t0);
|
|
486
|
+
return { status: res.status, latency_ms };
|
|
487
|
+
});
|
|
488
|
+
}
|