@sireai/optimus 0.1.35 → 0.1.37

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 (43) hide show
  1. package/dist/cli/optimus.js +41 -22
  2. package/dist/cli/optimus.js.map +1 -1
  3. package/dist/integrations/jira/jira-access-manager.d.ts +1 -0
  4. package/dist/integrations/jira/jira-access-manager.js +24 -0
  5. package/dist/integrations/jira/jira-access-manager.js.map +1 -1
  6. package/dist/problem-solving-core/codex/codex-runner.d.ts +2 -0
  7. package/dist/problem-solving-core/codex/codex-runner.js +43 -28
  8. package/dist/problem-solving-core/codex/codex-runner.js.map +1 -1
  9. package/dist/task-environment/delivery/task-delivery-service.js +3 -1
  10. package/dist/task-environment/delivery/task-delivery-service.js.map +1 -1
  11. package/dist/task-environment/intake/manual-problem-intake.js +30 -0
  12. package/dist/task-environment/intake/manual-problem-intake.js.map +1 -1
  13. package/dist/task-environment/orchestration/execution-context-assembler.d.ts +1 -0
  14. package/dist/task-environment/orchestration/execution-context-assembler.js +50 -0
  15. package/dist/task-environment/orchestration/execution-context-assembler.js.map +1 -1
  16. package/dist/task-environment/orchestration/task-orchestrator.d.ts +1 -0
  17. package/dist/task-environment/orchestration/task-orchestrator.js +27 -5
  18. package/dist/task-environment/orchestration/task-orchestrator.js.map +1 -1
  19. package/dist/task-environment/orchestration/task-package-inputs.d.ts +2 -0
  20. package/dist/task-environment/orchestration/task-package-inputs.js +38 -0
  21. package/dist/task-environment/orchestration/task-package-inputs.js.map +1 -0
  22. package/dist/task-environment/orchestration/task-runtime-policy.d.ts +4 -0
  23. package/dist/task-environment/orchestration/task-runtime-policy.js +38 -0
  24. package/dist/task-environment/orchestration/task-runtime-policy.js.map +1 -0
  25. package/dist/types.d.ts +32 -0
  26. package/embedded-skills/task/bugfix/android-hprof-analyzer/SKILL.md +37 -0
  27. package/embedded-skills/task/bugfix/android-hprof-analyzer/runtime/README.md +11 -0
  28. package/embedded-skills/task/bugfix/android-hprof-analyzer/scripts/analyze-hprof.mjs +286 -0
  29. package/embedded-skills/task/bugfix/android-hprof-analyzer/scripts/ensure-shark-runtime.mjs +213 -0
  30. package/embedded-skills/task/bugfix/android-hprof-analyzer/scripts/run-shark.sh +27 -0
  31. package/embedded-skills/task/bugfix/android-hprof-analyzer/skill.json +6 -0
  32. package/package.json +1 -1
  33. package/task-harnesses/bugfix/CONSTRAINTS.md +2 -0
  34. package/task-harnesses/bugfix/ROLE.md +4 -0
  35. package/task-harnesses/bugfix/STANDARD.md +1 -0
  36. package/task-harnesses/pm/ACCEPT.md +94 -0
  37. package/task-harnesses/pm/CONSTRAINTS.md +27 -0
  38. package/task-harnesses/pm/CONTEXT.md +26 -0
  39. package/task-harnesses/pm/EVOLUTION.md +35 -0
  40. package/task-harnesses/pm/ROLE.md +59 -0
  41. package/task-harnesses/pm/STANDARD.md +125 -0
  42. package/task-harnesses/pm/manifest.json +13 -0
  43. package/task-harnesses/registry.json +4 -0
@@ -0,0 +1,4 @@
1
+ import type { TaskRuntimePolicy } from "../../types.js";
2
+ export declare function getTaskRuntimePolicy(taskType: string): TaskRuntimePolicy;
3
+ export declare function taskRequiresRepository(taskType: string): boolean;
4
+ export declare function taskSupportsPublication(taskType: string): boolean;
@@ -0,0 +1,38 @@
1
+ const TASK_RUNTIME_POLICIES = {
2
+ bugfix: {
3
+ taskType: "bugfix",
4
+ executionBinding: "repo_bound",
5
+ artifactContract: "patch_result",
6
+ requiresRepository: true,
7
+ requiresRepoMemory: true,
8
+ supportsPublication: true,
9
+ supportsPatchArtifact: true,
10
+ primaryResultFile: "result.md",
11
+ expectedPrimaryArtifacts: ["result.md", "patch.diff"]
12
+ },
13
+ pm: {
14
+ taskType: "pm",
15
+ executionBinding: "artifact_only",
16
+ artifactContract: "prototype_result",
17
+ requiresRepository: false,
18
+ requiresRepoMemory: false,
19
+ supportsPublication: false,
20
+ supportsPatchArtifact: false,
21
+ primaryResultFile: "result.md",
22
+ expectedPrimaryArtifacts: ["result.md"]
23
+ }
24
+ };
25
+ export function getTaskRuntimePolicy(taskType) {
26
+ const policy = TASK_RUNTIME_POLICIES[taskType];
27
+ if (!policy) {
28
+ throw new Error(`No runtime policy registered for taskType ${taskType}.`);
29
+ }
30
+ return policy;
31
+ }
32
+ export function taskRequiresRepository(taskType) {
33
+ return getTaskRuntimePolicy(taskType).requiresRepository;
34
+ }
35
+ export function taskSupportsPublication(taskType) {
36
+ return getTaskRuntimePolicy(taskType).supportsPublication;
37
+ }
38
+ //# sourceMappingURL=task-runtime-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-runtime-policy.js","sourceRoot":"","sources":["../../../src/task-environment/orchestration/task-runtime-policy.ts"],"names":[],"mappings":"AAEA,MAAM,qBAAqB,GAAsC;IAC/D,MAAM,EAAE;QACN,QAAQ,EAAE,QAAQ;QAClB,gBAAgB,EAAE,YAAY;QAC9B,gBAAgB,EAAE,cAAc;QAChC,kBAAkB,EAAE,IAAI;QACxB,kBAAkB,EAAE,IAAI;QACxB,mBAAmB,EAAE,IAAI;QACzB,qBAAqB,EAAE,IAAI;QAC3B,iBAAiB,EAAE,WAAW;QAC9B,wBAAwB,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC;KACtD;IACD,EAAE,EAAE;QACF,QAAQ,EAAE,IAAI;QACd,gBAAgB,EAAE,eAAe;QACjC,gBAAgB,EAAE,kBAAkB;QACpC,kBAAkB,EAAE,KAAK;QACzB,kBAAkB,EAAE,KAAK;QACzB,mBAAmB,EAAE,KAAK;QAC1B,qBAAqB,EAAE,KAAK;QAC5B,iBAAiB,EAAE,WAAW;QAC9B,wBAAwB,EAAE,CAAC,WAAW,CAAC;KACxC;CACF,CAAC;AAEF,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,MAAM,MAAM,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,6CAA6C,QAAQ,GAAG,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,OAAO,oBAAoB,CAAC,QAAQ,CAAC,CAAC,kBAAkB,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,OAAO,oBAAoB,CAAC,QAAQ,CAAC,CAAC,mBAAmB,CAAC;AAC5D,CAAC"}
package/dist/types.d.ts CHANGED
@@ -106,6 +106,24 @@ export interface RuntimeEventContent {
106
106
  branch?: string;
107
107
  metadata?: Record<string, unknown>;
108
108
  }
109
+ export interface PmTaskInputReferenceMaterial {
110
+ type: "doc" | "image" | "link" | "note";
111
+ title: string;
112
+ content?: string;
113
+ url?: string;
114
+ }
115
+ export interface PmTaskInput {
116
+ requirementDocument: string;
117
+ productGoal?: string;
118
+ targetUser?: string;
119
+ coreFlow?: string;
120
+ prototypeScope?: string;
121
+ platform?: "web" | "mobile_web" | "ios" | "android" | "desktop" | "responsive";
122
+ constraints?: string[];
123
+ referenceMaterials?: PmTaskInputReferenceMaterial[];
124
+ styleDirection?: string;
125
+ changeNotes?: string;
126
+ }
109
127
  export interface FeishuAssigneeIdentity {
110
128
  displayName?: string;
111
129
  login?: string;
@@ -123,6 +141,19 @@ export interface PollingCheckpoint {
123
141
  skippedCount?: number;
124
142
  }
125
143
  export type RepositoryExecutionMode = "copy" | "inplace";
144
+ export type TaskExecutionBinding = "repo_bound" | "artifact_only";
145
+ export type TaskArtifactContract = "patch_result" | "prototype_result" | "analysis_result";
146
+ export interface TaskRuntimePolicy {
147
+ taskType: string;
148
+ executionBinding: TaskExecutionBinding;
149
+ artifactContract: TaskArtifactContract;
150
+ requiresRepository: boolean;
151
+ requiresRepoMemory: boolean;
152
+ supportsPublication: boolean;
153
+ supportsPatchArtifact: boolean;
154
+ primaryResultFile: string;
155
+ expectedPrimaryArtifacts: string[];
156
+ }
126
157
  export interface RepositoryRoot {
127
158
  path: string;
128
159
  alias?: string;
@@ -549,6 +580,7 @@ export interface RepositoryGuidanceContext {
549
580
  }
550
581
  export interface TaskExecutionContext {
551
582
  taskRootDir: string;
583
+ runtimePolicy: TaskRuntimePolicy;
552
584
  addresses: import("./task-environment/execution-addresses.js").ExecutionAddresses;
553
585
  sandboxMode: CodexSandboxMode;
554
586
  approvalPolicy: CodexApprovalPolicy;
@@ -0,0 +1,37 @@
1
+ # Android HPROF Analyzer
2
+
3
+ Use this skill for `bugfix` tasks when any evidence file name contains `hprof`.
4
+
5
+ ## Mandatory use
6
+ - If evidence contains a file whose basename includes `hprof`, run this skill before claiming a memory-leak root cause.
7
+ - Do not rely on screenshots alone when an HPROF file is available.
8
+ - Use the generated `summary.json` and `summary.md` as the primary heap-analysis fact source.
9
+
10
+ ## Input
11
+ - Preferred: an evidence manifest path plus an output directory.
12
+ - Optional: an explicit evidence directory and an obfuscation mapping file.
13
+ - The Shark CLI runtime is downloaded on demand into `~/.optimus/tools/shark/` the first time this skill runs.
14
+
15
+ ## Command
16
+ ```bash
17
+ node .agents/skills/android-hprof-analyzer/scripts/analyze-hprof.mjs \
18
+ --manifest <evidence-manifest-path> \
19
+ --output <artifactDir>/hprof-analysis
20
+ ```
21
+
22
+ Optional flags:
23
+ ```bash
24
+ --evidence-dir <dir>
25
+ --mapping <mapping-file>
26
+ ```
27
+
28
+ ## Output
29
+ - `summary.json`: structured analysis status and per-file metadata
30
+ - `summary.md`: concise human-readable summary with raw output references
31
+ - `raw/*.txt`: raw Shark CLI stdout/stderr for each analyzed dump
32
+
33
+ ## Rules
34
+ - Analyze every discovered evidence file whose basename contains `hprof`.
35
+ - If analysis fails, report whether the blocker was missing Java, missing embedded runtime, runner failure, or invalid dump.
36
+ - If first-use setup fails, report whether the download, checksum, or extraction step failed.
37
+ - Mention which HPROF file was analyzed in `result.md`.
@@ -0,0 +1,11 @@
1
+ This directory documents the runtime source used by the embedded
2
+ `android-hprof-analyzer` bugfix skill.
3
+
4
+ Source release:
5
+ - square/leakcanary `shark-cli-2.14.zip`
6
+
7
+ Runtime behavior:
8
+ - Optimus does not vendor the Shark CLI distribution inside the npm package.
9
+ - The skill downloads the pinned release on first use and caches it under
10
+ `~/.optimus/tools/shark/`.
11
+ - A local Java runtime is still required.
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from "node:child_process";
4
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
5
+ import { basename, dirname, extname, join, relative, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { promisify } from "node:util";
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const SCRIPT_DIR = resolve(dirname(fileURLToPath(import.meta.url)));
11
+ const RUNNER_PATH = join(SCRIPT_DIR, "run-shark.sh");
12
+
13
+ async function main() {
14
+ const options = parseArgs(process.argv.slice(2));
15
+ if (!options.outputDir) {
16
+ throw new Error("analyze-hprof requires --output <dir>.");
17
+ }
18
+ if (!options.manifestPath && !options.evidenceDir) {
19
+ throw new Error("analyze-hprof requires --manifest <path> or --evidence-dir <dir>.");
20
+ }
21
+
22
+ const outputDir = resolve(options.outputDir);
23
+ const rawDir = join(outputDir, "raw");
24
+ await mkdir(rawDir, { recursive: true });
25
+
26
+ const evidenceFiles = await discoverHprofFiles(options);
27
+ if (evidenceFiles.length === 0) {
28
+ const emptySummary = {
29
+ tool: "embedded-shark",
30
+ status: "failed",
31
+ files: [],
32
+ findings: [],
33
+ warnings: ["No evidence files whose basename contains 'hprof' were found."]
34
+ };
35
+ await writeOutputs(outputDir, emptySummary, ["No HPROF evidence files were found."]);
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+
40
+ const files = [];
41
+ const warnings = [];
42
+ for (const [index, filePath] of evidenceFiles.entries()) {
43
+ const rawPath = join(rawDir, `${String(index + 1).padStart(2, "0")}-${sanitizeName(basename(filePath))}.txt`);
44
+ const args = [];
45
+ if (options.mappingPath) {
46
+ args.push("--obfuscation-mapping", resolve(options.mappingPath));
47
+ }
48
+ args.push("--hprof", filePath, "analyze");
49
+
50
+ try {
51
+ const { stdout, stderr } = await execFileAsync(RUNNER_PATH, args, {
52
+ cwd: process.cwd(),
53
+ maxBuffer: 20 * 1024 * 1024,
54
+ env: process.env
55
+ });
56
+ const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
57
+ await writeFile(rawPath, combined || "(no output)\n", "utf8");
58
+ files.push({
59
+ path: filePath,
60
+ analyzed: true,
61
+ rawOutputPath: rawPath,
62
+ findings: extractFindingHints(combined)
63
+ });
64
+ } catch (error) {
65
+ const stdout = error?.stdout ? String(error.stdout) : "";
66
+ const stderr = error?.stderr ? String(error.stderr) : error instanceof Error ? error.message : String(error);
67
+ const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
68
+ await writeFile(rawPath, combined || "(no output)\n", "utf8");
69
+ const reason = classifyRunnerFailure(combined, error);
70
+ warnings.push(`${basename(filePath)}: ${reason}`);
71
+ files.push({
72
+ path: filePath,
73
+ analyzed: false,
74
+ rawOutputPath: rawPath,
75
+ error: reason,
76
+ findings: []
77
+ });
78
+ }
79
+ }
80
+
81
+ const findings = files.flatMap((entry) => entry.findings.map((finding) => ({
82
+ sourceFile: entry.path,
83
+ ...finding
84
+ })));
85
+ const analyzedCount = files.filter((entry) => entry.analyzed).length;
86
+ const status = analyzedCount === files.length
87
+ ? "ok"
88
+ : analyzedCount > 0
89
+ ? "partial"
90
+ : "failed";
91
+ const summary = {
92
+ tool: "embedded-shark",
93
+ status,
94
+ files,
95
+ findings,
96
+ warnings
97
+ };
98
+
99
+ const markdownLines = [
100
+ "# HPROF Analysis Summary",
101
+ "",
102
+ `- Status: ${status}`,
103
+ `- Files discovered: ${files.length}`,
104
+ `- Files analyzed: ${analyzedCount}`,
105
+ "",
106
+ "## Files"
107
+ ];
108
+ for (const entry of files) {
109
+ markdownLines.push(
110
+ `- ${relative(outputDir, entry.path) || entry.path}: ${entry.analyzed ? "analyzed" : `failed (${entry.error})`}`,
111
+ ` - Raw Output: ${relative(outputDir, entry.rawOutputPath) || entry.rawOutputPath}`
112
+ );
113
+ for (const finding of entry.findings) {
114
+ markdownLines.push(` - Finding: ${finding.target}${finding.retainPath ? ` | Path: ${finding.retainPath}` : ""}`);
115
+ }
116
+ }
117
+ if (warnings.length > 0) {
118
+ markdownLines.push("", "## Warnings");
119
+ for (const warning of warnings) {
120
+ markdownLines.push(`- ${warning}`);
121
+ }
122
+ }
123
+
124
+ await writeOutputs(outputDir, summary, markdownLines);
125
+ if (status === "failed") {
126
+ process.exitCode = 1;
127
+ }
128
+ }
129
+
130
+ function parseArgs(argv) {
131
+ const options = {};
132
+ for (let index = 0; index < argv.length; index += 1) {
133
+ const arg = argv[index];
134
+ const next = argv[index + 1];
135
+ if (!arg.startsWith("--")) {
136
+ continue;
137
+ }
138
+ if (!next || next.startsWith("--")) {
139
+ throw new Error(`Missing value for ${arg}.`);
140
+ }
141
+ if (arg === "--manifest") {
142
+ options.manifestPath = next;
143
+ index += 1;
144
+ continue;
145
+ }
146
+ if (arg === "--evidence-dir") {
147
+ options.evidenceDir = next;
148
+ index += 1;
149
+ continue;
150
+ }
151
+ if (arg === "--output") {
152
+ options.outputDir = next;
153
+ index += 1;
154
+ continue;
155
+ }
156
+ if (arg === "--mapping") {
157
+ options.mappingPath = next;
158
+ index += 1;
159
+ continue;
160
+ }
161
+ throw new Error(`Unknown argument: ${arg}`);
162
+ }
163
+ return options;
164
+ }
165
+
166
+ async function discoverHprofFiles(options) {
167
+ const discovered = new Set();
168
+ if (options.manifestPath) {
169
+ const manifestPath = resolve(options.manifestPath);
170
+ const manifestDir = dirname(manifestPath);
171
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
172
+ for (const value of collectPathValues(manifest)) {
173
+ await maybeAddFile(resolve(manifestDir, value), discovered);
174
+ }
175
+ await walkForHprofFiles(manifestDir, discovered);
176
+ }
177
+ if (options.evidenceDir) {
178
+ await walkForHprofFiles(resolve(options.evidenceDir), discovered);
179
+ }
180
+ return [...discovered].sort((left, right) => left.localeCompare(right));
181
+ }
182
+
183
+ function collectPathValues(node, values = []) {
184
+ if (!node) {
185
+ return values;
186
+ }
187
+ if (Array.isArray(node)) {
188
+ for (const item of node) {
189
+ collectPathValues(item, values);
190
+ }
191
+ return values;
192
+ }
193
+ if (typeof node === "object") {
194
+ for (const [key, value] of Object.entries(node)) {
195
+ if (typeof value === "string" && key.toLowerCase().endsWith("path")) {
196
+ values.push(value);
197
+ } else {
198
+ collectPathValues(value, values);
199
+ }
200
+ }
201
+ }
202
+ return values;
203
+ }
204
+
205
+ async function walkForHprofFiles(rootDir, discovered) {
206
+ let entries = [];
207
+ try {
208
+ entries = await readdir(rootDir, { withFileTypes: true });
209
+ } catch {
210
+ return;
211
+ }
212
+ for (const entry of entries) {
213
+ const entryPath = join(rootDir, entry.name);
214
+ if (entry.isDirectory()) {
215
+ await walkForHprofFiles(entryPath, discovered);
216
+ } else if (entry.isFile()) {
217
+ await maybeAddFile(entryPath, discovered);
218
+ }
219
+ }
220
+ }
221
+
222
+ async function maybeAddFile(filePath, discovered) {
223
+ if (!basename(filePath).toLowerCase().includes("hprof")) {
224
+ return;
225
+ }
226
+ try {
227
+ const info = await stat(filePath);
228
+ if (info.isFile()) {
229
+ discovered.add(filePath);
230
+ }
231
+ } catch {
232
+ // Ignore missing paths recorded in manifests.
233
+ }
234
+ }
235
+
236
+ function extractFindingHints(output) {
237
+ if (!output) {
238
+ return [];
239
+ }
240
+ const hints = [];
241
+ const retainedPattern = /([A-Za-z0-9_$.]+)\s+leaking/giu;
242
+ const pathPattern = /GC Root(?:.|\n){0,300}/iu;
243
+ const targets = [...output.matchAll(retainedPattern)].map((match) => match[1]).slice(0, 5);
244
+ const pathMatch = output.match(pathPattern)?.[0]?.replace(/\s+/gu, " ").trim();
245
+ for (const target of targets) {
246
+ hints.push({
247
+ target,
248
+ ...(pathMatch ? { retainPath: pathMatch } : {})
249
+ });
250
+ }
251
+ return hints;
252
+ }
253
+
254
+ function classifyRunnerFailure(output, error) {
255
+ const text = `${output}\n${error instanceof Error ? error.message : ""}`.toLowerCase();
256
+ if (text.includes("java runtime is required") || text.includes("no 'java' command could be found")) {
257
+ return "missing_java";
258
+ }
259
+ if (text.includes("embedded shark runner is unavailable")) {
260
+ return "missing_embedded_runtime";
261
+ }
262
+ if (text.includes("file not found") || text.includes("no such file")) {
263
+ return "missing_hprof_file";
264
+ }
265
+ if (text.includes("heap dump") || text.includes("hprof")) {
266
+ return "runner_failed";
267
+ }
268
+ return "runner_failed";
269
+ }
270
+
271
+ async function writeOutputs(outputDir, summary, markdownLines) {
272
+ await mkdir(outputDir, { recursive: true });
273
+ await writeFile(join(outputDir, "summary.json"), JSON.stringify(summary, null, 2), "utf8");
274
+ await writeFile(join(outputDir, "summary.md"), `${markdownLines.join("\n")}\n`, "utf8");
275
+ }
276
+
277
+ function sanitizeName(name) {
278
+ const extension = extname(name);
279
+ const base = extension ? name.slice(0, -extension.length) : name;
280
+ return `${base.replace(/[^\w.-]+/gu, "_").slice(0, 120) || "hprof"}${extension || ".txt"}`.replace(/\.txt$/u, "");
281
+ }
282
+
283
+ main().catch((error) => {
284
+ console.error(error instanceof Error ? error.message : String(error));
285
+ process.exitCode = 1;
286
+ });
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { createReadStream, createWriteStream } from "node:fs";
6
+ import { access, appendFile, chmod, copyFile, mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises";
7
+ import { get } from "node:https";
8
+ import { homedir, tmpdir } from "node:os";
9
+ import { dirname, join, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { promisify } from "node:util";
12
+
13
+ const execFileAsync = promisify(execFile);
14
+ const SHARK_VERSION = process.env.OPTIMUS_HPROF_SHARK_VERSION?.trim() || "2.14";
15
+ const ARCHIVE_NAME = `shark-cli-${SHARK_VERSION}.zip`;
16
+ const ARCHIVE_DIR_NAME = `shark-cli-${SHARK_VERSION}`;
17
+ const DEFAULT_ARCHIVE_URL = `https://github.com/square/leakcanary/releases/download/shark-cli-${SHARK_VERSION}/${ARCHIVE_NAME}`;
18
+ const DEFAULT_ARCHIVE_SHA256 = "4a1022a4610fd6a4a1306b264f95985c4210e169e2bd4b0ad19bbdcc16d6beef";
19
+ const SCRIPT_DIR = resolve(dirname(fileURLToPath(import.meta.url)));
20
+ const LOG_PREFIX = "[optimus][hprof-runtime]";
21
+ let logFilePathPromise;
22
+
23
+ async function main() {
24
+ const cacheRoot = resolve(
25
+ process.env.OPTIMUS_HPROF_CACHE_DIR?.trim()
26
+ || join(process.env.HOME?.trim() || homedir(), ".optimus", "tools", "shark")
27
+ );
28
+ const archiveUrl = process.env.OPTIMUS_HPROF_SHARK_URL?.trim() || DEFAULT_ARCHIVE_URL;
29
+ const archiveSha256 = process.env.OPTIMUS_HPROF_SHARK_SHA256?.trim() || DEFAULT_ARCHIVE_SHA256;
30
+ const archivePathOverride = process.env.OPTIMUS_HPROF_SHARK_ARCHIVE?.trim();
31
+ const installDir = join(cacheRoot, ARCHIVE_DIR_NAME);
32
+ const runnerPath = join(installDir, "bin", "shark-cli");
33
+
34
+ if (await isExecutableFile(runnerPath)) {
35
+ logInfo(`Using cached Shark runtime at ${installDir}.`);
36
+ process.stdout.write(runnerPath);
37
+ return;
38
+ }
39
+
40
+ await mkdir(cacheRoot, { recursive: true });
41
+ const tempRoot = await mkdtemp(join(tmpdir(), "optimus-shark-install-"));
42
+ logInfo(`Preparing Shark runtime ${SHARK_VERSION} under ${cacheRoot}.`);
43
+
44
+ try {
45
+ const archivePath = archivePathOverride ? resolve(archivePathOverride) : join(tempRoot, ARCHIVE_NAME);
46
+ if (!archivePathOverride) {
47
+ logInfo(`Downloading Shark runtime ${SHARK_VERSION} from ${archiveUrl}.`);
48
+ await downloadArchive(archiveUrl, archivePath);
49
+ logInfo(`Downloaded Shark archive to ${archivePath}.`);
50
+ } else {
51
+ logInfo(`Using provided Shark archive at ${archivePath}.`);
52
+ }
53
+ logInfo(`Verifying Shark archive checksum (${archiveSha256.slice(0, 12)}...).`);
54
+ await verifyArchiveChecksum(archivePath, archiveSha256);
55
+ logInfo("Checksum verified.");
56
+
57
+ const extractRoot = join(tempRoot, "extract");
58
+ await mkdir(extractRoot, { recursive: true });
59
+ logInfo(`Extracting Shark archive into ${extractRoot}.`);
60
+ await extractArchive(archivePath, extractRoot);
61
+ logInfo("Archive extracted.");
62
+
63
+ const extractedDir = join(extractRoot, ARCHIVE_DIR_NAME);
64
+ const extractedRunnerPath = join(extractedDir, "bin", "shark-cli");
65
+ if (!(await isExecutableFile(extractedRunnerPath))) {
66
+ throw new Error(`Downloaded Shark archive did not contain an executable runner at ${extractedRunnerPath}.`);
67
+ }
68
+ await chmod(extractedRunnerPath, 0o755);
69
+
70
+ const stagingDir = join(cacheRoot, `.installing-${Date.now()}-${process.pid}`);
71
+ await rm(stagingDir, { recursive: true, force: true });
72
+ await rename(extractedDir, stagingDir);
73
+ try {
74
+ await rename(stagingDir, installDir);
75
+ logInfo(`Installed Shark runtime at ${installDir}.`);
76
+ } catch (error) {
77
+ if (await isExecutableFile(runnerPath)) {
78
+ await rm(stagingDir, { recursive: true, force: true });
79
+ logInfo(`Another process finished installation first; reusing cached runtime at ${installDir}.`);
80
+ } else {
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ process.stdout.write(runnerPath);
86
+ } finally {
87
+ await rm(tempRoot, { recursive: true, force: true });
88
+ }
89
+ }
90
+
91
+ async function downloadArchive(url, destinationPath) {
92
+ if (url.startsWith("file://")) {
93
+ await copyFile(fileURLToPath(url), destinationPath);
94
+ return;
95
+ }
96
+ if (!url.startsWith("https://")) {
97
+ throw new Error(`Unsupported Shark archive URL: ${url}`);
98
+ }
99
+
100
+ await new Promise((resolvePromise, rejectPromise) => {
101
+ const request = get(url, (response) => {
102
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
103
+ response.resume();
104
+ downloadArchive(response.headers.location, destinationPath).then(resolvePromise, rejectPromise);
105
+ return;
106
+ }
107
+ if (response.statusCode !== 200) {
108
+ response.resume();
109
+ rejectPromise(new Error(`download_failed: HTTP ${response.statusCode ?? "unknown"}`));
110
+ return;
111
+ }
112
+
113
+ const output = createWriteStream(destinationPath);
114
+ output.on("finish", () => {
115
+ output.close();
116
+ resolvePromise(undefined);
117
+ });
118
+ output.on("error", rejectPromise);
119
+ response.on("error", rejectPromise);
120
+ response.pipe(output);
121
+ });
122
+ request.on("error", rejectPromise);
123
+ });
124
+ }
125
+
126
+ async function verifyArchiveChecksum(archivePath, expectedSha256) {
127
+ const actualSha256 = await computeSha256(archivePath);
128
+ if (actualSha256 !== expectedSha256.toLowerCase()) {
129
+ throw new Error(`checksum_mismatch: expected ${expectedSha256}, received ${actualSha256}`);
130
+ }
131
+ }
132
+
133
+ async function computeSha256(filePath) {
134
+ const hash = createHash("sha256");
135
+ await new Promise((resolvePromise, rejectPromise) => {
136
+ const input = createReadStream(filePath);
137
+ input.on("data", (chunk) => hash.update(chunk));
138
+ input.on("end", resolvePromise);
139
+ input.on("error", rejectPromise);
140
+ });
141
+ return hash.digest("hex");
142
+ }
143
+
144
+ async function extractArchive(archivePath, extractRoot) {
145
+ try {
146
+ await execFileAsync("unzip", ["-oq", archivePath, "-d", extractRoot], {
147
+ cwd: SCRIPT_DIR,
148
+ maxBuffer: 16 * 1024 * 1024
149
+ });
150
+ } catch (error) {
151
+ throw new Error(`extract_failed: ${formatProcessError(error)}`.trim());
152
+ }
153
+ }
154
+
155
+ function formatProcessError(error) {
156
+ if (!error || typeof error !== "object") {
157
+ return "";
158
+ }
159
+ const stdout = "stdout" in error && error.stdout ? String(error.stdout).trim() : "";
160
+ const stderr = "stderr" in error && error.stderr ? String(error.stderr).trim() : "";
161
+ return [stdout, stderr].filter(Boolean).join(" ").slice(0, 500);
162
+ }
163
+
164
+ async function isExecutableFile(filePath) {
165
+ try {
166
+ await access(filePath);
167
+ const info = await stat(filePath);
168
+ return info.isFile();
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ function logInfo(message) {
175
+ void writeLogLine(message);
176
+ }
177
+
178
+ main().catch((error) => {
179
+ const message = error instanceof Error ? error.message : String(error);
180
+ void writeLogLine(`Failed to prepare Shark runtime: ${message}`);
181
+ process.exitCode = 1;
182
+ });
183
+
184
+ async function writeLogLine(message) {
185
+ const line = `${LOG_PREFIX} ${message}`;
186
+ process.stderr.write(`${line}\n`);
187
+ try {
188
+ const logPath = await resolveLogFilePath();
189
+ await mkdir(dirname(logPath), { recursive: true });
190
+ await appendFile(logPath, `${new Date().toISOString()} ${line}\n`, "utf8");
191
+ } catch {
192
+ // Best-effort logging only; stderr remains the primary fallback.
193
+ }
194
+ }
195
+
196
+ function resolveLogFilePath() {
197
+ if (!logFilePathPromise) {
198
+ const homeDir = process.env.HOME?.trim() || homedir();
199
+ const runtimeLogsDir = process.env.OPTIMUS_RUNTIME_LOGS_DIR?.trim()
200
+ ? resolve(process.env.OPTIMUS_RUNTIME_LOGS_DIR)
201
+ : join(homeDir, ".optimus", "runtime", "logs");
202
+ logFilePathPromise = Promise.resolve(join(runtimeLogsDir, `runtime-${currentDatePart()}.log`));
203
+ }
204
+ return logFilePathPromise;
205
+ }
206
+
207
+ function currentDatePart() {
208
+ const now = new Date();
209
+ const year = String(now.getFullYear());
210
+ const month = String(now.getMonth() + 1).padStart(2, "0");
211
+ const day = String(now.getDate()).padStart(2, "0");
212
+ return `${year}-${month}-${day}`;
213
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
5
+ ENSURE_SCRIPT="${SCRIPT_DIR}/ensure-shark-runtime.mjs"
6
+
7
+ if [[ -n "${OPTIMUS_HPROF_SHARK_RUNNER:-}" ]]; then
8
+ RUNNER="${OPTIMUS_HPROF_SHARK_RUNNER}"
9
+ else
10
+ if ! command -v node >/dev/null 2>&1; then
11
+ echo "Node.js is required to provision the embedded Shark runtime but was not found in PATH." >&2
12
+ exit 2
13
+ fi
14
+ RUNNER="$(node "${ENSURE_SCRIPT}")"
15
+ fi
16
+
17
+ if [[ ! -x "${RUNNER}" ]]; then
18
+ echo "Embedded Shark runner is unavailable: ${RUNNER}" >&2
19
+ exit 2
20
+ fi
21
+
22
+ if ! command -v java >/dev/null 2>&1; then
23
+ echo "Java runtime is required for embedded Shark analysis but was not found in PATH." >&2
24
+ exit 3
25
+ fi
26
+
27
+ exec "${RUNNER}" "$@"
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "android-hprof-analyzer",
3
+ "level": "task",
4
+ "version": "1.0.0",
5
+ "taskTypes": ["bugfix"]
6
+ }