@lumpcode/cli 0.0.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.
@@ -0,0 +1,25 @@
1
+ export const command = (({ prompt, stepVariables }) => {
2
+ const { model = 'auto' } = stepVariables || {};
3
+
4
+ if (!prompt) return null;
5
+
6
+ return {
7
+ executable: 'copilot',
8
+ args: [
9
+ '-p',
10
+ prompt,
11
+ '--allow-all-tools',
12
+ '--silent',
13
+ '--model',
14
+ model,
15
+ ],
16
+ };
17
+ });
18
+
19
+ export const setup = (async ({}) => {
20
+ return {};
21
+ });
22
+
23
+ export const teardown = (() => {
24
+ return;
25
+ });
@@ -0,0 +1,70 @@
1
+ import { execAsync } from '@lumpcode/core';
2
+ import { defineCommand, defineCommandSetup, defineCommandTeardown } from "@lumpcode/cli-types";
3
+
4
+ export const command = defineCommand(async ({ prompt, stepVariables, contextRunState, stepIndex }) => {
5
+ const { model = 'auto', newChat = false, chatIdIndex = null } = stepVariables || {};
6
+
7
+ const chatState = contextRunState.cursorSetup ?? (contextRunState.cursorSetup = {});
8
+ const chatKey = Array.isArray(stepIndex) ? stepIndex.join('.') : String(stepIndex);
9
+
10
+ let chatId = chatIdIndex != null
11
+ ? chatState.chatsIds?.[chatIdIndex]
12
+ : chatState.setupChatId;
13
+
14
+ if (!chatId) {
15
+ throw new Error(
16
+ chatIdIndex != null
17
+ ? `Chat ID not found for index: ${chatIdIndex}`
18
+ : 'Chat ID not found in cursor setup state',
19
+ );
20
+ }
21
+
22
+ if (newChat) {
23
+ const createChatResult = await execAsync("cursor-agent create-chat");
24
+
25
+ if (!createChatResult.success) {
26
+ throw new Error(`Failed to create chat: ${createChatResult.data.message}`);
27
+ }
28
+
29
+ chatId = (createChatResult.data.stdout || createChatResult.data.stderr).trim();
30
+
31
+ chatState.chatsIds ??= {};
32
+ chatState.chatsIds[chatKey] = chatId;
33
+ }
34
+
35
+ return {
36
+ executable: 'cursor-agent',
37
+ args: [
38
+ '-p',
39
+ `"${prompt}"`,
40
+ '--force',
41
+ '--model',
42
+ model,
43
+ '--resume',
44
+ chatId,
45
+ ],
46
+ }
47
+ });
48
+
49
+ export const setup = defineCommandSetup(async ({}) => {
50
+ const setupChatId = await execAsync("cursor-agent create-chat");
51
+
52
+ if (!setupChatId.success) {
53
+ throw new Error(`Failed to create chat: ${setupChatId.data.message}`);
54
+ }
55
+
56
+ const setupChatIdStr = (setupChatId.data.stdout || setupChatId.data.stderr).trim();
57
+
58
+ return {
59
+ contextRunState: {
60
+ setupChatId: setupChatIdStr,
61
+ chatsIds: {
62
+ "0": setupChatIdStr,
63
+ }
64
+ }
65
+ };
66
+ });
67
+
68
+ export const teardown = defineCommandTeardown((() => {
69
+ return;
70
+ }));
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "localConfig.schema.json",
4
+ "title": "Lumpcode local configuration",
5
+ "description": "Schema for .lumpcode/local.json — a gitignored, per-machine file that controls where and how Lumpcode runs lumps from this checkout. Created by `lumpcode project-setup`; edit it on a daemon machine (e.g. switch `mode` to \"dedicated\").",
6
+ "type": "object",
7
+ "required": ["mode", "projectBaseBranch"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "mode": {
11
+ "type": "string",
12
+ "enum": ["shared", "dedicated"],
13
+ "description": "How Lumpcode handles the current checkout. `shared`: this is your day-to-day clone; Lumpcode never touches it and runs in a separate copy under ~/.lumpcode/project-copies/<projectName>/. `dedicated`: this clone is owned by Lumpcode (typical daemon machine); Lumpcode pulls and runs in place, including a destructive `git reset --hard` on `projectBaseBranch` before each tick."
14
+ },
15
+ "projectBaseBranch": {
16
+ "type": "string",
17
+ "description": "Branch Lumpcode pulls (and resets to) before running any lump. Also the default `baseBranch` for lumps that don't override it. Status checks (`finished`) compare against `origin/<projectBaseBranch>`."
18
+ },
19
+ "workspaceStrategy": {
20
+ "type": "string",
21
+ "enum": ["checkout", "worktree"],
22
+ "description": "How each lump run prepares git inside the preflight workspace. `checkout` (default): switch the main worktree to a fresh lump branch. `worktree`: run in a linked worktree under `.lumpcode/worktrees/<branch>/` so the main worktree stays on `projectBaseBranch`."
23
+ },
24
+ "disabled": {
25
+ "type": "boolean",
26
+ "description": "When true, the background daemon (`lumpcode start`) skips every lump on this machine without stopping the scheduler. Manual `lumpcode run` is unaffected."
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,195 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "lumpConfig.schema.json",
4
+ "title": "Lump JSON Configuration",
5
+ "description": "JSON configuration schema for a Lump — a long-running automated coding campaign managed by Lumpcode. This schema defines the JSON-serializable subset of the full Lump configuration (LumpJsonConfig). Function-valued fields from the JS config are either excluded entirely or replaced by file paths pointing to modules that export the corresponding function.",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "allOf": [
9
+ {
10
+ "oneOf": [
11
+ { "required": ["contextListJson"], "not": { "anyOf": [{ "required": ["getContextListFn"] }, { "required": ["contextMatchFn"] }] } },
12
+ { "required": ["getContextListFn"], "not": { "anyOf": [{ "required": ["contextListJson"] }, { "required": ["contextMatchFn"] }] } },
13
+ { "required": ["contextMatchFn"], "not": { "anyOf": [{ "required": ["contextListJson"] }, { "required": ["getContextListFn"] }] } }
14
+ ],
15
+ "description": "Exactly one context source must be provided: 'contextListJson' (a static json or path to a json file), 'getContextListFn' (a file path to a dynamic context-fetching function), or 'contextMatchFn' (a file path to a MatchFn that builds contexts by scanning codebase files)."
16
+ },
17
+ {
18
+ "oneOf": [
19
+ { "required": ["prompt"], "not": { "required": ["steps"] } },
20
+ { "required": ["steps"], "not": { "required": ["prompt"] } }
21
+ ],
22
+ "description": "Exactly one prompt definition must be provided: either 'prompt' (a single prompt item) or 'steps' (an ordered list of prompt steps)."
23
+ }
24
+ ],
25
+ "properties": {
26
+ "baseBranch": {
27
+ "type": "string",
28
+ "description": "Optional override of the project-wide `projectBaseBranch` (from `.lumpcode/local.json`). When set, this lump runs against the given branch instead; the per-lump workspace flow will fetch and pull it before creating the lump branch. Most lumps omit this field.",
29
+ "examples": ["main", "develop", "release/2.0"]
30
+ },
31
+ "branchFn": {
32
+ "type": "string",
33
+ "description": "File path to a module that default-exports a BranchFn — a function that determines the branch name for a given set of contexts. In the JS config this can be the function itself; in JSON it must be a file path to the module.",
34
+ "examples": ["./lump/branchFn.ts", "./lump/branchFn.js"]
35
+ },
36
+ "command": {
37
+ "type": "string",
38
+ "description": "A top-level command shorthand applied to all prompt items. Can be either:\n • A registered command string (e.g. 'claude') that will receive the prompt\n • A file path to a module containing a CommandFn\nWhen set here, individual prompt items inherit this command unless they override it.",
39
+ "examples": ["claude", "aider", "./command.ts"]
40
+ },
41
+ "contextListJson": {
42
+ "oneOf": [
43
+ {
44
+ "type": "string",
45
+ "description": "File path to a JSON file containing a { [variableName]: pathTemplate } mapping."
46
+ },
47
+ {
48
+ "type": "object",
49
+ "additionalProperties": {
50
+ "type": "string"
51
+ },
52
+ "description": "Inline mapping from variable name to path template. Each value is a path template with {PLACEHOLDER} captures (e.g. 'src/{NAME}.ts'); each key is the variable name exposed to the prompt as {KEY} / @{KEY}."
53
+ }
54
+ ],
55
+ "description": "Static context source. The engine scans the tree and, for every path that matches a template, builds a context whose variables map keys (e.g. 'FILE') to the matched path. Path templates support {PLACEHOLDER} captures and $modifier{PLACEHOLDER} (e.g. $upperFirst{NAME}) for naming-convention checks. Prefer paths without a leading './'. Mutually exclusive with getContextListFn / contextMatchFn.",
56
+ "examples": [
57
+ "./contextList.json",
58
+ {
59
+ "FILE": "src/{NAME}.ts"
60
+ },
61
+ {
62
+ "FOLDER": "src/components/{COMPONENT_NAME}/",
63
+ "INDEX": "src/components/{COMPONENT_NAME}/index.ts",
64
+ "TYPES": "src/components/{COMPONENT_NAME}/{COMPONENT_NAME}.types.ts",
65
+ "COMPONENT": "src/components/{COMPONENT_NAME}/$upperFirst{COMPONENT_NAME}.tsx"
66
+ }
67
+ ]
68
+ },
69
+ "contextMatchFn": {
70
+ "type": "string",
71
+ "description": "File path to a module exporting a MatchFn — a function that builds contexts by scanning codebase files. For each codeBasePath, the function returns an object { contextName, filePathVariableName, moreContextVariables?, contextOptions? } or null if the file doesn't match. Multiple matches with the same contextName merge into one context (variables accumulate; later matches override duplicate keys). This is an alternative to 'contextListJson' and 'getContextListFn' for dynamically grouping files into contexts based on custom matching logic.",
72
+ "examples": ["./lump/contextMatchFn.ts"]
73
+ },
74
+ "contextOptionsFn": {
75
+ "type": "string",
76
+ "description": "File path to a module exporting a function that provides additional options or configuration for each context. In the JS config this can be the function itself; in JSON it must be a file path.",
77
+ "examples": ["./lump/contextOptionsFn.ts"]
78
+ },
79
+ "disabled": {
80
+ "type": "boolean",
81
+ "description": "When true, this lump configuration is skipped during execution. Useful for temporarily disabling a lump without removing its configuration.",
82
+ "default": false
83
+ },
84
+ "getContextListFn": {
85
+ "type": "string",
86
+ "description": "File path to a module exporting a GetContextListFn — a function that dynamically retrieves the list of contexts to process. In the JS config this can be the function itself; in JSON it must be a file path. Use contextListJson instead if your context list is static.",
87
+ "examples": ["./lump/getContextListFn.ts"]
88
+ },
89
+ "maximumNumberOfConcurrentBranches": {
90
+ "type": "integer",
91
+ "minimum": 1,
92
+ "description": "Maximum number of context branches that can be processed concurrently. Limits parallelism to avoid overwhelming the system or hitting rate limits."
93
+ },
94
+ "numberOfContextsPerBranch": {
95
+ "type": "integer",
96
+ "minimum": 1,
97
+ "description": "Number of contexts to group into a single branch. Defaults to 1, meaning each context gets its own branch.",
98
+ "default": 1
99
+ },
100
+ "lumpVariables": {
101
+ "type": "object",
102
+ "additionalProperties": true,
103
+ "description": "An arbitrary key-value object of variables passed through to prompt functions, setup/teardown hooks, and other lifecycle functions. Use this to parameterize your lump without changing code.",
104
+ "examples": [
105
+ { "language": "TypeScript", "style": "functional" }
106
+ ]
107
+ },
108
+ "verbose": {
109
+ "type": "boolean",
110
+ "description": "When true, enables verbose logging output during lump execution for debugging purposes.",
111
+ "default": false
112
+ },
113
+ "registerCommands": {
114
+ "type": "array",
115
+ "items": {
116
+ "type": "string"
117
+ },
118
+ "description": "An array of command names to pre-register before processing prompt items. Required when prompt items are defined dynamically (via functions) and reference commands by name, since dynamic prompt items cannot trigger command registration at build time. Each name is resolved to a command module file (e.g. 'cursor-agent' resolves to 'commands/cursor-agent.js').",
119
+ "examples": [["claude-code"], ["cursor-agent", "aider"]]
120
+ },
121
+ "prompt": {
122
+ "$ref": "#/$defs/LumpJsonConfigStep",
123
+ "description": "A single prompt item defining the prompt template or function, the command to run, and optional lifecycle hooks. Use this shorthand for simple lumps that only require one prompt step. For multi-step lumps, use 'steps' instead."
124
+ },
125
+ "steps": {
126
+ "type": "array",
127
+ "items": {
128
+ "oneOf": [
129
+ {
130
+ "$ref": "#/$defs/LumpJsonConfigStep"
131
+ },
132
+ {
133
+ "type": "string",
134
+ "description": "A shorthand string interpreted as a prompt template — either inline prompt text or a file path to a template file. Equivalent to a LumpJsonConfigStep with only the 'promptTemplate' field set."
135
+ }
136
+ ]
137
+ },
138
+ "description": "An ordered list of prompt items executed sequentially for each context. Each element can be:\n • A full LumpJsonConfigStep object with explicit fields\n • A string shorthand interpreted as a promptTemplate (inline text or file path)\nUse this for multi-step lumps that need multiple sequential prompt/command cycles per context."
139
+ }
140
+ },
141
+ "$defs": {
142
+ "LumpJsonConfigStep": {
143
+ "type": ["object", "string"],
144
+ "additionalProperties": false,
145
+ "description": "A single step in a lump's execution pipeline. Defines an optional prompt, which command to execute, and optional post-processing. Prompt fields are optional; when omitted the command receives an empty prompt string. The top-level 'command' is used when this step does not set 'command'.",
146
+ "oneOf": [
147
+ { "type": "string", "description": "Shorthand: inline prompt template or file path to a template file." },
148
+ {
149
+ "type": "object",
150
+ "not": { "required": ["promptTemplate", "promptFn"] }
151
+ }
152
+ ],
153
+ "properties": {
154
+ "promptTemplate": {
155
+ "type": "string",
156
+ "description": "The prompt content for this step. Accepts two forms:\n • Inline template string — the literal prompt text, which may contain braced placeholders from the context's variables: `{VAR}` (literal) and `@{VAR}` (same value with a leading `@` for agents that use `@path` file context). Double-brace forms like {{FILE}} are NOT substituted.\n • File path — a path to a template file whose contents will be read and used as the prompt (e.g. './prompts/refactor.md')\nFor dynamic prompt generation based on context at runtime, use 'promptFn' instead.",
157
+ "examples": [
158
+ "Refactor the following file to use modern syntax",
159
+ "./prompts/my-template.md",
160
+ "./prompts/refactor.txt"
161
+ ]
162
+ },
163
+ "promptFn": {
164
+ "type": "string",
165
+ "description": "File path to a module exporting a PromptFn — a function that dynamically generates prompt text at runtime. The function signature is:\n (params: { context, stepIndex, contextRunState, lumpVariables, stepVariables? }) => string | Promise<string>\nUse this instead of 'promptTemplate' when the prompt needs to vary based on context, run state, or other dynamic data.",
166
+ "examples": ["./lump/promptFn.ts", "./lump/promptFn.js"]
167
+ },
168
+ "command": {
169
+ "type": "string",
170
+ "description": "The command to execute with the generated prompt. Accepts two forms:\n • Registered command name — the bare name of a command module (e.g. 'claude', 'aider'). Lumpcode resolves it to '.lumpcode/commands/<name>.js' (project-local) or '~/.lumpcode/commands/<name>.js' (global). Do NOT include flags here; agent-specific flags go inside the command module's CommandFn output.\n • File path — a path to a module exporting a CommandFn directly\nThe prompt text is passed by the command module as args to the underlying agent.",
171
+ "examples": ["claude", "aider", "./lump/command.ts"]
172
+ },
173
+ "postCommandExecFn": {
174
+ "type": "string",
175
+ "description": "File path to a module exporting a PostCommandExecFn — a function called after the command finishes executing. The function receives:\n { commandResult, context, prompt, stepIndex, contextRunState, lumpVariables }\nUse this for validation, logging, cleanup, or other side effects after each prompt/command cycle.",
176
+ "examples": ["./lump/postCommandExecFn.ts"]
177
+ },
178
+ "stepVariables": {
179
+ "type": "object",
180
+ "additionalProperties": true,
181
+ "description": "An arbitrary key-value object of variables specific to this prompt item. Passed to the promptFn alongside the global lumpVariables, enabling per-step parameterization without modifying the top-level lumpVariables.",
182
+ "examples": [
183
+ { "maxRetries": 3, "style": "concise" }
184
+ ]
185
+ },
186
+ "timeoutMillis": {
187
+ "type": "integer",
188
+ "minimum": 0,
189
+ "description": "Maximum time in milliseconds to wait for the command to complete before it is considered timed out. If omitted or set to 0, no timeout is enforced.",
190
+ "examples": [60000, 300000]
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@lumpcode/cli",
3
+ "version": "0.0.0",
4
+ "description": "CLI for Lumpcode — automate batched coding work with CLI coding agents over contexts using git for tracking",
5
+ "license": "Apache-2.0",
6
+ "author": "",
7
+ "type": "commonjs",
8
+ "main": "dist/index.js",
9
+ "engines": {
10
+ "node": ">=22"
11
+ },
12
+ "bin": {
13
+ "lumpcode": "./bin/lumpcode.js"
14
+ },
15
+ "files": [
16
+ "bin/lumpcode.js",
17
+ "dist/index.js",
18
+ "dist/schemas",
19
+ "dist/presets",
20
+ "scripts/native-binary.mjs",
21
+ "scripts/postinstall.mjs",
22
+ "README.md",
23
+ "LICENSE",
24
+ "DOCS"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "clean": "rm -rf dist && rm -rf bin",
31
+ "build:bundle": "node scripts/build.js",
32
+ "build:npm": "npm run build:bundle",
33
+ "build:sea": "npm run build:bundle && bash scripts/build-sea.sh",
34
+ "build:sea:windows": "npm run build:bundle && powershell -ExecutionPolicy Bypass -File scripts/build-sea.ps1",
35
+ "build": "npm run clean && npm run build:sea",
36
+ "postinstall": "node scripts/postinstall.mjs",
37
+ "prepublishOnly": "npm run build:bundle",
38
+ "check-types": "tsc --noEmit",
39
+ "test": "vitest run --config vitest.config.ts",
40
+ "test:e2e": "node scripts/run-e2e.mjs",
41
+ "test:e2e:node": "node scripts/run-e2e.mjs --node",
42
+ "test:e2e:ci": "node scripts/run-e2e.mjs --ci",
43
+ "test:e2e:ci:node": "node scripts/run-e2e.mjs --node --ci"
44
+ },
45
+ "dependencies": {
46
+ "@lumpcode/core": "^0.0.0",
47
+ "ajv": "^8.20.0",
48
+ "commander": "^9.5.0",
49
+ "croner": "^9.1.0",
50
+ "dotenv": "^16.6.1",
51
+ "lodash": "^4.18.1",
52
+ "node-machine-id": "^1.1.12"
53
+ },
54
+ "devDependencies": {
55
+ "@types/lodash": "^4.17.24",
56
+ "@vercel/ncc": "^0.38.4",
57
+ "postject": "^1.0.0-alpha.6",
58
+ "tslib": "^2.8.1",
59
+ "typescript": "^5.7.2"
60
+ }
61
+ }
@@ -0,0 +1,180 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+
5
+ const DEFAULT_INSTALL_REPO = 'YOUR_ORG/Lumpcode';
6
+
7
+ /**
8
+ * @param {string} [platform]
9
+ * @param {string} [arch]
10
+ * @returns {{ platform: string; arch: string; assetBase: string } | null}
11
+ */
12
+ export function detectPlatformArch(platform = process.platform, arch = process.arch) {
13
+ let archName;
14
+ if (arch === 'arm64' || arch === 'aarch64') {
15
+ archName = 'arm64';
16
+ } else if (arch === 'x64' || arch === 'x86_64') {
17
+ archName = 'x64';
18
+ } else {
19
+ return null;
20
+ }
21
+
22
+ let platformName;
23
+ if (platform === 'linux') {
24
+ platformName = 'linux';
25
+ } else if (platform === 'darwin') {
26
+ platformName = 'darwin';
27
+ } else if (platform === 'win32') {
28
+ platformName = 'windows';
29
+ } else {
30
+ return null;
31
+ }
32
+
33
+ if (platformName === 'windows' && archName !== 'x64') {
34
+ return null;
35
+ }
36
+
37
+ const assetBase =
38
+ platformName === 'windows'
39
+ ? `lumpcode-${platformName}-${archName}.exe`
40
+ : `lumpcode-${platformName}-${archName}`;
41
+
42
+ return { platform: platformName, arch: archName, assetBase };
43
+ }
44
+
45
+ /**
46
+ * @param {{ version: string; assetBase: string; repo?: string }} input
47
+ * @returns {string}
48
+ */
49
+ export function getReleaseDownloadUrl({ version, assetBase, repo = process.env.LUMPCODE_INSTALL_REPO || DEFAULT_INSTALL_REPO }) {
50
+ const tag = version.startsWith('v') ? version : `v${version}`;
51
+ return `https://github.com/${repo}/releases/download/${tag}/${assetBase}`;
52
+ }
53
+
54
+ /**
55
+ * @param {string} pkgRoot
56
+ * @returns {string}
57
+ */
58
+ export function getVendorBinaryPath(pkgRoot) {
59
+ return process.platform === 'win32'
60
+ ? path.join(pkgRoot, 'vendor', 'lumpcode.exe')
61
+ : path.join(pkgRoot, 'vendor', 'lumpcode');
62
+ }
63
+
64
+ /**
65
+ * @param {string} pkgRoot
66
+ * @returns {boolean}
67
+ */
68
+ export function isNativeBinaryInstalled(pkgRoot) {
69
+ const marker = path.join(pkgRoot, 'vendor', '.installed');
70
+ const binary = getVendorBinaryPath(pkgRoot);
71
+ return fs.existsSync(marker) && fs.existsSync(binary);
72
+ }
73
+
74
+ /**
75
+ * @param {string} src
76
+ * @param {string} dest
77
+ */
78
+ function copyDirRecursive(src, dest) {
79
+ fs.mkdirSync(dest, { recursive: true });
80
+ fs.cpSync(src, dest, { recursive: true });
81
+ }
82
+
83
+ /**
84
+ * @param {string} data
85
+ * @param {string} expectedHex
86
+ * @returns {boolean}
87
+ */
88
+ function verifySha256(data, expectedHex) {
89
+ const actual = crypto.createHash('sha256').update(data).digest('hex');
90
+ return actual === expectedHex.trim().toLowerCase();
91
+ }
92
+
93
+ /**
94
+ * @param {string} url
95
+ * @param {typeof fetch} fetchFn
96
+ * @returns {Promise<{ ok: true; buffer: Buffer } | { ok: false; status?: number; error?: string }>}
97
+ */
98
+ async function downloadUrl(url, fetchFn) {
99
+ try {
100
+ const response = await fetchFn(url);
101
+ if (!response.ok) {
102
+ return { ok: false, status: response.status };
103
+ }
104
+ const arrayBuffer = await response.arrayBuffer();
105
+ return { ok: true, buffer: Buffer.from(arrayBuffer) };
106
+ } catch (error) {
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ return { ok: false, error: message };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * @param {{
114
+ * pkgRoot: string;
115
+ * version: string;
116
+ * platform?: string;
117
+ * arch?: string;
118
+ * fetchFn?: typeof fetch;
119
+ * }} input
120
+ * @returns {Promise<{ installed: boolean; reason?: string; assetBase?: string }>}
121
+ */
122
+ export async function installNativeBinary({
123
+ pkgRoot,
124
+ version,
125
+ platform = process.platform,
126
+ arch = process.arch,
127
+ fetchFn = fetch,
128
+ }) {
129
+ const detected = detectPlatformArch(platform, arch);
130
+ if (!detected) {
131
+ return { installed: false, reason: 'unsupported-platform' };
132
+ }
133
+
134
+ const { assetBase } = detected;
135
+ const url = getReleaseDownloadUrl({ version, assetBase });
136
+ const download = await downloadUrl(url, fetchFn);
137
+ if (!download.ok) {
138
+ return {
139
+ installed: false,
140
+ reason: download.status === 404 ? 'not-found' : 'download-failed',
141
+ assetBase,
142
+ };
143
+ }
144
+
145
+ const checksumUrl = `${url}.sha256`;
146
+ const checksumDownload = await downloadUrl(checksumUrl, fetchFn);
147
+ if (checksumDownload.ok) {
148
+ const expectedHex = checksumDownload.buffer.toString('utf-8').split(/\s+/)[0];
149
+ if (!verifySha256(download.buffer, expectedHex)) {
150
+ return { installed: false, reason: 'checksum-mismatch', assetBase };
151
+ }
152
+ }
153
+
154
+ const vendorDir = path.join(pkgRoot, 'vendor');
155
+ const binaryPath = getVendorBinaryPath(pkgRoot);
156
+ const schemasSrc = path.join(pkgRoot, 'dist', 'schemas');
157
+ const presetsSrc = path.join(pkgRoot, 'dist', 'presets');
158
+
159
+ if (!fs.existsSync(schemasSrc) || !fs.existsSync(presetsSrc)) {
160
+ return { installed: false, reason: 'missing-dist-sidecars', assetBase };
161
+ }
162
+
163
+ fs.mkdirSync(vendorDir, { recursive: true });
164
+ fs.writeFileSync(binaryPath, download.buffer);
165
+ if (platform !== 'win32') {
166
+ fs.chmodSync(binaryPath, 0o755);
167
+ }
168
+
169
+ copyDirRecursive(schemasSrc, path.join(vendorDir, 'schemas'));
170
+ copyDirRecursive(presetsSrc, path.join(vendorDir, 'presets'));
171
+
172
+ const marker = {
173
+ version,
174
+ assetBase,
175
+ installedAt: new Date().toISOString(),
176
+ };
177
+ fs.writeFileSync(path.join(vendorDir, '.installed'), `${JSON.stringify(marker)}\n`, 'utf-8');
178
+
179
+ return { installed: true, assetBase };
180
+ }
@@ -0,0 +1,52 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { installNativeBinary } from './native-binary.mjs';
6
+
7
+ const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+
9
+ function shouldSkipPostinstall() {
10
+ if (process.env.LUMPCODE_SKIP_BINARY === '1') {
11
+ return true;
12
+ }
13
+ if (process.env.npm_config_ignore_scripts === 'true') {
14
+ return true;
15
+ }
16
+ if (process.env.CI === 'true') {
17
+ return true;
18
+ }
19
+ if (!fs.existsSync(path.join(pkgRoot, 'dist', 'index.js'))) {
20
+ return true;
21
+ }
22
+ if (fs.existsSync(path.join(pkgRoot, 'src', 'root.ts'))) {
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+
28
+ function logVerbose(message) {
29
+ if (process.env.LUMPCODE_INSTALL_VERBOSE === '1') {
30
+ console.log(`[lumpcode] ${message}`);
31
+ }
32
+ }
33
+
34
+ async function main() {
35
+ if (shouldSkipPostinstall()) {
36
+ logVerbose('postinstall: skipped native binary download');
37
+ return;
38
+ }
39
+
40
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'));
41
+ const result = await installNativeBinary({ pkgRoot, version: pkg.version });
42
+
43
+ if (result.installed) {
44
+ logVerbose(`postinstall: installed native binary (${result.assetBase})`);
45
+ } else {
46
+ logVerbose(`postinstall: native binary not available (${result.reason ?? 'unknown'}), using Node fallback`);
47
+ }
48
+ }
49
+
50
+ main().catch((error) => {
51
+ logVerbose(`postinstall: native binary install failed (${error instanceof Error ? error.message : String(error)}), using Node fallback`);
52
+ });