@alisio/core 0.8.0 → 0.10.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/dist/analysis/data/engine-client.d.ts +2 -0
- package/dist/analysis/data/engine-client.js +11 -4
- package/dist/application.d.ts +11 -1
- package/dist/application.js +46 -0
- package/dist/changelog/data.js +42 -0
- package/dist/commands/builtins.js +6 -0
- package/dist/commands/catalog.d.ts +4 -2
- package/dist/commands/catalog.js +23 -1
- package/dist/config.d.ts +31 -0
- package/dist/config.js +31 -1
- package/dist/context/elide.d.ts +50 -0
- package/dist/context/elide.js +222 -0
- package/dist/context/format.d.ts +21 -0
- package/dist/context/format.js +81 -0
- package/dist/context/governance.d.ts +58 -0
- package/dist/context/governance.js +96 -0
- package/dist/context/invariants.d.ts +88 -0
- package/dist/context/invariants.js +141 -0
- package/dist/context/inventory.d.ts +22 -0
- package/dist/context/inventory.js +178 -0
- package/dist/context/report.d.ts +35 -0
- package/dist/context/report.js +120 -0
- package/dist/context/sources.d.ts +20 -0
- package/dist/context/sources.js +41 -0
- package/dist/context/types.d.ts +43 -0
- package/dist/context/types.js +5 -0
- package/dist/context/usage.d.ts +96 -0
- package/dist/context/usage.js +264 -0
- package/dist/context/util.d.ts +22 -0
- package/dist/context/util.js +36 -0
- package/dist/core/compaction.d.ts +7 -2
- package/dist/core/compaction.js +9 -3
- package/dist/core/contracts.d.ts +8 -0
- package/dist/core/runner.d.ts +67 -0
- package/dist/core/runner.js +272 -10
- package/dist/eval/compare.d.ts +1 -1
- package/dist/eval/compare.js +18 -1
- package/dist/eval/engine.js +31 -1
- package/dist/eval/report.js +44 -1
- package/dist/forge/javascript/hosts/process-host.d.ts +1 -1
- package/dist/forge/javascript/hosts/process-host.js +4 -1
- package/dist/forge/python/wasm-node.d.ts +1 -1
- package/dist/forge/python/wasm-node.js +4 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +8 -2
- package/dist/plugins/host.d.ts +5 -0
- package/dist/plugins/host.js +15 -1
- package/dist/plugins/install.d.ts +50 -2
- package/dist/plugins/install.js +102 -12
- package/dist/plugins/local-install.d.ts +33 -0
- package/dist/plugins/local-install.js +121 -0
- package/dist/plugins/npm-runner.d.ts +30 -0
- package/dist/plugins/npm-runner.js +66 -0
- package/dist/plugins/tarball.d.ts +39 -0
- package/dist/plugins/tarball.js +277 -0
- package/dist/plugins/version-compat.d.ts +20 -0
- package/dist/plugins/version-compat.js +154 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export { BackgroundTaskStore, isLive as isTaskLive, isTerminal as isTaskTerminal
|
|
|
32
32
|
export { changelogNews, compareVersions, formatChangelogMarkdown, isVersion, loadChangelog, type NewsDecision, parseChangelog, type SelectedEntries, selectEntries, UNRELEASED, } from "./changelog/index.ts";
|
|
33
33
|
export { BUILTIN_COMMANDS, type BuiltinCommand, CommandCatalog, type CommandExecutionContext, type CommandHost, type CommandResult, type CommandSurface, } from "./commands/catalog.ts";
|
|
34
34
|
export { type Config, type ConfigLoadResult, type ConfigProvenance, configFile, configHome, configSchema, isSettableSettingKey, type LoadedConfig, loadConfig, loadConfigWithProvenance, type McpServerSource, OCI_IMAGE_PATTERN, overridesSavedProviderProfile, type SettableSettingInfo, type SettableSettingKey, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.ts";
|
|
35
|
+
export { formatContextGovernorStatsSection, formatContextReport, notableSegments, } from "./context/format.ts";
|
|
36
|
+
export { buildContextReport, MAX_REPORT_SEGMENTS } from "./context/report.ts";
|
|
35
37
|
export { type ContextBudget, checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.ts";
|
|
36
38
|
export type { ApprovalDecision, ApprovalHandler, ApprovalRequest, ArtifactPublisherFactory, BeginRunInput, CapabilityGate, ContextSource, CoreArtifactPublisher, DecisionsFactory, EndRunInput, EventPage, HookFailure, MessagePage, PageOptions, Policy, PublishedArtifactInfo, RunnerExtensions, RunRecord, RunStatus, Session, SessionStore, StoredEvent, TerminalRunStatus, ToolCallMeta, } from "./core/contracts.ts";
|
|
37
39
|
export { HumanWaits } from "./core/human-wait.ts";
|
|
@@ -71,8 +73,12 @@ export { describePermissionStatus, type EffectState, effectState, FULL_ACCESS_WA
|
|
|
71
73
|
export { DIAGRAM_MAX_BYTES, DIAGRAM_MAX_NODES, DIAGRAM_SYNTAXES, DIAGRAMS_MAX_ALLOWED, mermaidProblem, type PlanDiagram, validateDiagrams, } from "./plan/diagrams.ts";
|
|
72
74
|
export { buildPlanManifest, parsePlanHeadings, slugifyHeading, } from "./plan/manifest.ts";
|
|
73
75
|
export { claimApprovedPlan, currentPlan, discardApprovedPlan, IMPLEMENTATION_AGENT_ID, implementationPrompt, PLAN_CONTEXT_MAX_CHARS, PLAN_CRITERIA_KEY, PLAN_OPTIONS, PLAN_QUESTION_ID, PLAN_REVIEW_TITLE, PLAN_TEXT_KEY, type PlanFollowUp, type PlanState, type PlanStatus, planHash, readPlanState, settlePlanRun, withdrawPendingPlan, } from "./plan/state.ts";
|
|
74
|
-
export { discoverPlugins, type PluginEnvironment, PluginHost, type PluginHostOptions, pluginPrefix, type ViewInfo, ViewRunError, } from "./plugins/host.ts";
|
|
75
|
-
export { type CliInstallOptions, cliInstall, cliUpdateAll, INSTALL_TIMEOUT_MS, type InstallPluginInput, type InstallPluginResult, type InstallRunner, installedGlobalPluginPackages, installedNpmPlugins, installPlugin, isPeerConflict, npmInstallTarget, type PluginSpec, parsePluginSpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, updateAllPlugins, } from "./plugins/install.ts";
|
|
76
|
+
export { discoverPlugins, type PluginEnvironment, PluginHost, type PluginHostOptions, pluginCodeImportedFrom, pluginPrefix, type ViewInfo, ViewRunError, } from "./plugins/host.ts";
|
|
77
|
+
export { type CliInstallOptions, cliInstall, cliUpdateAll, INSTALL_TIMEOUT_MS, type InstallPluginInput, type InstallPluginResult, type InstallRunner, installedGlobalPluginPackages, installedGlobalPlugins, installedNpmPlugins, installPlugin, isPeerConflict, npmInstallTarget, type PluginSpec, parsePluginSpec, pluginDependencySpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, type UninstallPluginInput, type UninstallPluginResult, uninstallPlugin, updateAllPlugins, } from "./plugins/install.ts";
|
|
78
|
+
export { type InstallLocalPluginInput, type InstallLocalPluginResult, installLocalPluginTarball, LocalInstallError, type LocalInstallPhase, storedTarballName, } from "./plugins/local-install.ts";
|
|
79
|
+
export { NPM_NOT_FOUND_MESSAGE, type NpmInvocation, resolveNpmInvocation, runNpm, } from "./plugins/npm-runner.ts";
|
|
80
|
+
export { inspectPluginTarball, MAX_TARBALL_BYTES, type PluginTarballInfo, readTarballPackageJson, type TarballLimits, } from "./plugins/tarball.ts";
|
|
81
|
+
export { type CatalogPluginState, type CatalogPluginStateInput, catalogPluginState, comparePluginVersions, satisfiesRange, } from "./plugins/version-compat.ts";
|
|
76
82
|
export * from "./proof/index.ts";
|
|
77
83
|
export { ActiveProvider, ProviderRegistry, UnconfiguredProvider } from "./providers/registry.ts";
|
|
78
84
|
export { availableProviderModels, type ProviderModelCatalog, resolveProviderModel, } from "./providers/routing.ts";
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,8 @@ export { BackgroundTaskStore, isLive as isTaskLive, isTerminal as isTaskTerminal
|
|
|
32
32
|
export { changelogNews, compareVersions, formatChangelogMarkdown, isVersion, loadChangelog, parseChangelog, selectEntries, UNRELEASED, } from "./changelog/index.js";
|
|
33
33
|
export { BUILTIN_COMMANDS, CommandCatalog, } from "./commands/catalog.js";
|
|
34
34
|
export { configFile, configHome, configSchema, isSettableSettingKey, loadConfig, loadConfigWithProvenance, OCI_IMAGE_PATTERN, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.js";
|
|
35
|
+
export { formatContextGovernorStatsSection, formatContextReport, notableSegments, } from "./context/format.js";
|
|
36
|
+
export { buildContextReport, MAX_REPORT_SEGMENTS } from "./context/report.js";
|
|
35
37
|
export { checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.js";
|
|
36
38
|
export { HumanWaits } from "./core/human-wait.js";
|
|
37
39
|
export { EXIT_PLAN_TOOL, GOAL_TOOLS, OPT_IN_TOOLS } from "./core/opt-in.js";
|
|
@@ -70,8 +72,12 @@ export { describePermissionStatus, effectState, FULL_ACCESS_WARNING, modeFromFla
|
|
|
70
72
|
export { DIAGRAM_MAX_BYTES, DIAGRAM_MAX_NODES, DIAGRAM_SYNTAXES, DIAGRAMS_MAX_ALLOWED, mermaidProblem, validateDiagrams, } from "./plan/diagrams.js";
|
|
71
73
|
export { buildPlanManifest, parsePlanHeadings, slugifyHeading, } from "./plan/manifest.js";
|
|
72
74
|
export { claimApprovedPlan, currentPlan, discardApprovedPlan, IMPLEMENTATION_AGENT_ID, implementationPrompt, PLAN_CONTEXT_MAX_CHARS, PLAN_CRITERIA_KEY, PLAN_OPTIONS, PLAN_QUESTION_ID, PLAN_REVIEW_TITLE, PLAN_TEXT_KEY, planHash, readPlanState, settlePlanRun, withdrawPendingPlan, } from "./plan/state.js";
|
|
73
|
-
export { discoverPlugins, PluginHost, pluginPrefix, ViewRunError, } from "./plugins/host.js";
|
|
74
|
-
export { cliInstall, cliUpdateAll, INSTALL_TIMEOUT_MS, installedGlobalPluginPackages, installedNpmPlugins, installPlugin, isPeerConflict, npmInstallTarget, parsePluginSpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, updateAllPlugins, } from "./plugins/install.js";
|
|
75
|
+
export { discoverPlugins, PluginHost, pluginCodeImportedFrom, pluginPrefix, ViewRunError, } from "./plugins/host.js";
|
|
76
|
+
export { cliInstall, cliUpdateAll, INSTALL_TIMEOUT_MS, installedGlobalPluginPackages, installedGlobalPlugins, installedNpmPlugins, installPlugin, isPeerConflict, npmInstallTarget, parsePluginSpec, pluginDependencySpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, uninstallPlugin, updateAllPlugins, } from "./plugins/install.js";
|
|
77
|
+
export { installLocalPluginTarball, LocalInstallError, storedTarballName, } from "./plugins/local-install.js";
|
|
78
|
+
export { NPM_NOT_FOUND_MESSAGE, resolveNpmInvocation, runNpm, } from "./plugins/npm-runner.js";
|
|
79
|
+
export { inspectPluginTarball, MAX_TARBALL_BYTES, readTarballPackageJson, } from "./plugins/tarball.js";
|
|
80
|
+
export { catalogPluginState, comparePluginVersions, satisfiesRange, } from "./plugins/version-compat.js";
|
|
75
81
|
export * from "./proof/index.js";
|
|
76
82
|
export { ActiveProvider, ProviderRegistry, UnconfiguredProvider } from "./providers/registry.js";
|
|
77
83
|
export { availableProviderModels, resolveProviderModel, } from "./providers/routing.js";
|
package/dist/plugins/host.d.ts
CHANGED
|
@@ -37,6 +37,11 @@ export interface ViewInfo {
|
|
|
37
37
|
description: string;
|
|
38
38
|
params: Record<string, unknown>;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether any plugin file under `dir` was already imported in this process. Such code stays cached
|
|
42
|
+
* after the package is updated on disk, so the new version only loads after a restart.
|
|
43
|
+
*/
|
|
44
|
+
export declare function pluginCodeImportedFrom(dir: string): boolean;
|
|
40
45
|
export declare class PluginHost implements RunnerExtensions {
|
|
41
46
|
private registry;
|
|
42
47
|
private state;
|
package/dist/plugins/host.js
CHANGED
|
@@ -9,7 +9,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { mkdirSync } from "node:fs";
|
|
11
11
|
import { readdir, stat } from "node:fs/promises";
|
|
12
|
-
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
14
14
|
import Ajv from "ajv";
|
|
15
15
|
import { z } from "zod";
|
|
@@ -53,6 +53,19 @@ export class ViewRunError extends Error {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
const message = (error) => (error instanceof Error ? error.message : String(error));
|
|
56
|
+
/** Entry files of plugins imported by this process (Node caches an ES module for its lifetime). */
|
|
57
|
+
const importedPluginFiles = new Set();
|
|
58
|
+
/**
|
|
59
|
+
* Whether any plugin file under `dir` was already imported in this process. Such code stays cached
|
|
60
|
+
* after the package is updated on disk, so the new version only loads after a restart.
|
|
61
|
+
*/
|
|
62
|
+
export function pluginCodeImportedFrom(dir) {
|
|
63
|
+
const prefix = dir.endsWith(sep) ? dir : `${dir}${sep}`;
|
|
64
|
+
for (const file of importedPluginFiles)
|
|
65
|
+
if (file.startsWith(prefix))
|
|
66
|
+
return true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
56
69
|
export class PluginHost {
|
|
57
70
|
registry;
|
|
58
71
|
state;
|
|
@@ -222,6 +235,7 @@ export class PluginHost {
|
|
|
222
235
|
let module;
|
|
223
236
|
try {
|
|
224
237
|
module = await import(__rewriteRelativeImportExtension(pathToFileURL(path).href));
|
|
238
|
+
importedPluginFiles.add(path);
|
|
225
239
|
}
|
|
226
240
|
catch (error) {
|
|
227
241
|
if (error.code === "ERR_UNKNOWN_FILE_EXTENSION")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ToolRegistry } from "../core/registry.ts";
|
|
2
|
-
import {
|
|
2
|
+
import type { ProcessResult } from "../runtime/process.ts";
|
|
3
3
|
export interface PluginSpec {
|
|
4
4
|
name: string;
|
|
5
5
|
version?: string;
|
|
@@ -22,6 +22,7 @@ export type InstallRunner = (command: string, args: string[], options: {
|
|
|
22
22
|
env?: Record<string, string>;
|
|
23
23
|
}) => Promise<ProcessResult>;
|
|
24
24
|
export declare const INSTALL_TIMEOUT_MS = 300000;
|
|
25
|
+
export declare const defaultInstallRunner: InstallRunner;
|
|
25
26
|
export interface InstallPluginInput {
|
|
26
27
|
/** `npm:<package>[@<version>]` or a bare package name. */
|
|
27
28
|
spec: string;
|
|
@@ -29,6 +30,11 @@ export interface InstallPluginInput {
|
|
|
29
30
|
configHome?: string;
|
|
30
31
|
/** Refresh an already-installed package to `@latest`, keeping the npm name in the config. */
|
|
31
32
|
update?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Run npm even when the package is already installed, at exactly `spec`'s version (an explicit
|
|
35
|
+
* reinstall or downgrade from the catalog). `update` instead takes `@latest`.
|
|
36
|
+
*/
|
|
37
|
+
force?: boolean;
|
|
32
38
|
/** Refuse immediately; loading and installing executable plugins is disabled under --read-only. */
|
|
33
39
|
readOnly?: boolean;
|
|
34
40
|
signal?: AbortSignal;
|
|
@@ -48,7 +54,9 @@ export interface InstallPluginResult {
|
|
|
48
54
|
pluginsDir: string;
|
|
49
55
|
/** Already present in the global plugins directory before this call (no npm run performed). */
|
|
50
56
|
alreadyInstalled: boolean;
|
|
51
|
-
/**
|
|
57
|
+
/** Version that was installed before this call, when there was one. */
|
|
58
|
+
previousVersion?: string;
|
|
59
|
+
/** Refreshed (to `@latest`, or reinstalled at the requested version) by this call. */
|
|
52
60
|
updated: boolean;
|
|
53
61
|
/** Plain-text remark about where the plugin loads and how project trust applies. */
|
|
54
62
|
trustRemark: string;
|
|
@@ -65,8 +73,48 @@ export declare function isPeerConflict(output: string): boolean;
|
|
|
65
73
|
* `<pluginsDir>/node_modules` (names only; file/directory entries are ignored).
|
|
66
74
|
*/
|
|
67
75
|
export declare function installedGlobalPluginPackages(home: string): Promise<string[]>;
|
|
76
|
+
/** Atomically adds `packageName` to the global configuration `plugins` array, preserving all other fields. */
|
|
77
|
+
export declare function persistGlobalPluginEntry(configHomeDir: string, packageName: string): Promise<string>;
|
|
78
|
+
/** Atomically removes `packageName` from the global configuration `plugins` array. */
|
|
79
|
+
export declare function removeGlobalPluginEntry(configHomeDir: string, packageName: string): Promise<boolean>;
|
|
80
|
+
/** The dependency spec npm recorded for `name` in `<pluginsDir>/package.json`, if any. */
|
|
81
|
+
export declare function pluginDependencySpec(pluginsDir: string, name: string): Promise<string | undefined>;
|
|
82
|
+
export declare const LOCAL_PACKAGES_DIR = "local-packages";
|
|
83
|
+
/**
|
|
84
|
+
* The stored tarball a `file:` dependency spec points at, only when it lives inside
|
|
85
|
+
* `<pluginsDir>/local-packages` (anything else, including `..` escapes, is never deleted by us).
|
|
86
|
+
*/
|
|
87
|
+
export declare function storedTarballOf(pluginsDir: string, spec: string | undefined): string | undefined;
|
|
68
88
|
/** The shared install routine used by both the CLI command and the `plugin_install` tool. */
|
|
69
89
|
export declare function installPlugin(input: InstallPluginInput): Promise<InstallPluginResult>;
|
|
90
|
+
/** Globally installed plugin packages (configured and present on disk) with their versions. */
|
|
91
|
+
export declare function installedGlobalPlugins(home: string): Promise<Array<{
|
|
92
|
+
name: string;
|
|
93
|
+
version: string;
|
|
94
|
+
}>>;
|
|
95
|
+
export interface UninstallPluginInput {
|
|
96
|
+
/** The npm package name (no `npm:` prefix, no version). */
|
|
97
|
+
packageName: string;
|
|
98
|
+
/** Defaults to `configHome()` (ALISIO_CONFIG_HOME, else ~/.config/alisio). */
|
|
99
|
+
configHome?: string;
|
|
100
|
+
readOnly?: boolean;
|
|
101
|
+
signal?: AbortSignal;
|
|
102
|
+
/** Injected npm for tests; defaults to the real (or bundled) npm. */
|
|
103
|
+
runner?: InstallRunner;
|
|
104
|
+
}
|
|
105
|
+
export interface UninstallPluginResult {
|
|
106
|
+
packageName: string;
|
|
107
|
+
/** The package was installed or configured before this call (false = nothing to do). */
|
|
108
|
+
removed: boolean;
|
|
109
|
+
pluginsDir: string;
|
|
110
|
+
configFile: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Removes a package from the global plugins directory (`npm uninstall --prefix`) and its name from
|
|
114
|
+
* the global configuration `plugins` array. Idempotent: an absent package only cleans a stale
|
|
115
|
+
* config entry. The config is untouched when npm fails.
|
|
116
|
+
*/
|
|
117
|
+
export declare function uninstallPlugin(input: UninstallPluginInput): Promise<UninstallPluginResult>;
|
|
70
118
|
export interface UpdateAllPluginsInput {
|
|
71
119
|
configHome?: string;
|
|
72
120
|
readOnly?: boolean;
|
package/dist/plugins/install.js
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
* still follows the existing executable-plugin policy: it runs in-process wherever it is loaded and
|
|
10
10
|
* is disabled entirely under `--read-only`.
|
|
11
11
|
*/
|
|
12
|
-
import { chmod, mkdir, open, readdir, rename, unlink } from "node:fs/promises";
|
|
12
|
+
import { chmod, mkdir, open, readdir, rename, rm, unlink } from "node:fs/promises";
|
|
13
13
|
import { dirname, join, resolve } from "node:path";
|
|
14
14
|
import { textResult } from "@alisio/sdk";
|
|
15
15
|
import { configHome } from "../config.js";
|
|
16
16
|
import { readJson } from "../runtime/fs.js";
|
|
17
17
|
import { PLUGIN_KEYWORD } from "../runtime/modules.js";
|
|
18
|
-
import {
|
|
18
|
+
import { runNpm } from "./npm-runner.js";
|
|
19
19
|
/** npm package name: scope (optional) followed by the name; both start with an alphanumeric. */
|
|
20
20
|
const PACKAGE_PART = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
21
21
|
const NAME_RE = new RegExp(`^(?:@${PACKAGE_PART}\\/)?${PACKAGE_PART}$`);
|
|
@@ -67,7 +67,7 @@ export function npmInstallTarget(name, version, update = false) {
|
|
|
67
67
|
return update ? `${name}@latest` : version ? `${name}@${version}` : name;
|
|
68
68
|
}
|
|
69
69
|
export const INSTALL_TIMEOUT_MS = 300_000;
|
|
70
|
-
const
|
|
70
|
+
export const defaultInstallRunner = (command, args, options) => runNpm(command, args, options, { timeoutMs: INSTALL_TIMEOUT_MS, maxBytes: 64_000 });
|
|
71
71
|
/**
|
|
72
72
|
* Redacts probable secrets from npm output (auth tokens, JWTs, bearer credentials, userinfo in
|
|
73
73
|
* URLs) and caps the size, so a failing install never echoes credentials to the user.
|
|
@@ -124,20 +124,29 @@ single) {
|
|
|
124
124
|
return new Error(`${head}Retry with: ${retry}` +
|
|
125
125
|
(single ? `\n(or: alisio install npm:${single})` : "\n(or: alisio install --update)"));
|
|
126
126
|
}
|
|
127
|
-
/**
|
|
128
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Atomically rewrites the global configuration `plugins` array through `update`, preserving every
|
|
129
|
+
* other field. Returns the file path and whether the array changed (no write when it did not).
|
|
130
|
+
*/
|
|
131
|
+
async function updateGlobalPluginEntries(configHomeDir, update, options) {
|
|
129
132
|
const file = join(configHomeDir, "config.json");
|
|
130
133
|
const existing = await readJson(file);
|
|
131
134
|
let raw;
|
|
132
|
-
if (existing === undefined)
|
|
135
|
+
if (existing === undefined) {
|
|
136
|
+
if (!options.createIfMissing)
|
|
137
|
+
return { file, changed: false };
|
|
133
138
|
raw = {};
|
|
139
|
+
}
|
|
134
140
|
else if (typeof existing === "object" && !Array.isArray(existing))
|
|
135
141
|
raw = existing;
|
|
136
142
|
else
|
|
137
143
|
throw new Error(`Global Alisio configuration must be a JSON object: ${file}`);
|
|
138
144
|
const current = Array.isArray(raw.plugins) ? raw.plugins : [];
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
const next = update(current);
|
|
146
|
+
const changed = next.length !== current.length || next.some((entry, i) => entry !== current[i]);
|
|
147
|
+
if (!changed && existing !== undefined)
|
|
148
|
+
return { file, changed: false };
|
|
149
|
+
raw.plugins = next;
|
|
141
150
|
raw.schemaVersion ??= 1;
|
|
142
151
|
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
143
152
|
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
@@ -157,8 +166,38 @@ async function persistGlobalPluginEntry(configHomeDir, packageName) {
|
|
|
157
166
|
await unlink(temporary).catch(() => { });
|
|
158
167
|
throw error;
|
|
159
168
|
}
|
|
169
|
+
return { file, changed: true };
|
|
170
|
+
}
|
|
171
|
+
/** Atomically adds `packageName` to the global configuration `plugins` array, preserving all other fields. */
|
|
172
|
+
export async function persistGlobalPluginEntry(configHomeDir, packageName) {
|
|
173
|
+
const { file } = await updateGlobalPluginEntries(configHomeDir, (current) => (current.includes(packageName) ? current : [...current, packageName]), { createIfMissing: true });
|
|
160
174
|
return file;
|
|
161
175
|
}
|
|
176
|
+
/** Atomically removes `packageName` from the global configuration `plugins` array. */
|
|
177
|
+
export async function removeGlobalPluginEntry(configHomeDir, packageName) {
|
|
178
|
+
const { changed } = await updateGlobalPluginEntries(configHomeDir, (current) => current.filter((entry) => entry !== packageName), { createIfMissing: false });
|
|
179
|
+
return changed;
|
|
180
|
+
}
|
|
181
|
+
/** The dependency spec npm recorded for `name` in `<pluginsDir>/package.json`, if any. */
|
|
182
|
+
export async function pluginDependencySpec(pluginsDir, name) {
|
|
183
|
+
const manifest = (await readJson(join(pluginsDir, "package.json")).catch(() => undefined));
|
|
184
|
+
const spec = manifest?.dependencies?.[name];
|
|
185
|
+
return typeof spec === "string" ? spec : undefined;
|
|
186
|
+
}
|
|
187
|
+
export const LOCAL_PACKAGES_DIR = "local-packages";
|
|
188
|
+
/**
|
|
189
|
+
* The stored tarball a `file:` dependency spec points at, only when it lives inside
|
|
190
|
+
* `<pluginsDir>/local-packages` (anything else, including `..` escapes, is never deleted by us).
|
|
191
|
+
*/
|
|
192
|
+
export function storedTarballOf(pluginsDir, spec) {
|
|
193
|
+
if (!spec?.startsWith("file:"))
|
|
194
|
+
return undefined;
|
|
195
|
+
const target = resolve(pluginsDir, spec.slice(5));
|
|
196
|
+
const root = join(pluginsDir, LOCAL_PACKAGES_DIR);
|
|
197
|
+
if (dirname(target) !== root || !/\.(tgz|tar\.gz)$/.test(target))
|
|
198
|
+
return undefined;
|
|
199
|
+
return target;
|
|
200
|
+
}
|
|
162
201
|
/** The shared install routine used by both the CLI command and the `plugin_install` tool. */
|
|
163
202
|
export async function installPlugin(input) {
|
|
164
203
|
const parsed = parsePluginSpec(input.spec);
|
|
@@ -172,9 +211,9 @@ export async function installPlugin(input) {
|
|
|
172
211
|
const installedManifest = (await readJson(join(packageDir, "package.json")));
|
|
173
212
|
const alreadyInstalled = !!installedManifest?.version;
|
|
174
213
|
let updated = false;
|
|
175
|
-
if (!alreadyInstalled || input.update) {
|
|
214
|
+
if (!alreadyInstalled || input.update || input.force) {
|
|
176
215
|
const signal = input.signal ?? AbortSignal.timeout(INSTALL_TIMEOUT_MS);
|
|
177
|
-
const run = input.runner ??
|
|
216
|
+
const run = input.runner ?? defaultInstallRunner;
|
|
178
217
|
const npmResult = await run("npm", [
|
|
179
218
|
"install",
|
|
180
219
|
"--prefix",
|
|
@@ -183,7 +222,7 @@ export async function installPlugin(input) {
|
|
|
183
222
|
], { cwd: pluginsDir, signal });
|
|
184
223
|
if (npmResult.exitCode !== 0)
|
|
185
224
|
throw npmInstallError(`plugin "${parsed.name}"`, [npmInstallTarget(parsed.name, parsed.version, input.update)], pluginsDir, npmResult, await installedGlobalPluginPackages(home), npmInstallTarget(parsed.name, parsed.version));
|
|
186
|
-
updated = !!input.update && alreadyInstalled;
|
|
225
|
+
updated = (!!input.update || !!input.force) && alreadyInstalled;
|
|
187
226
|
}
|
|
188
227
|
const manifest = (await readJson(join(packageDir, "package.json")));
|
|
189
228
|
const installedVersion = manifest?.version ?? "unknown";
|
|
@@ -196,10 +235,61 @@ export async function installPlugin(input) {
|
|
|
196
235
|
configFile,
|
|
197
236
|
pluginsDir,
|
|
198
237
|
alreadyInstalled,
|
|
238
|
+
...(installedManifest?.version ? { previousVersion: installedManifest.version } : {}),
|
|
199
239
|
updated,
|
|
200
240
|
trustRemark: pluginTrustRemark(parsed.name),
|
|
201
241
|
};
|
|
202
242
|
}
|
|
243
|
+
/** Globally installed plugin packages (configured and present on disk) with their versions. */
|
|
244
|
+
export async function installedGlobalPlugins(home) {
|
|
245
|
+
const result = [];
|
|
246
|
+
for (const name of await installedGlobalPluginPackages(home)) {
|
|
247
|
+
const manifest = (await readJson(join(home, "plugins", "node_modules", name, "package.json")));
|
|
248
|
+
result.push({ name, version: manifest?.version ?? "unknown" });
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Removes a package from the global plugins directory (`npm uninstall --prefix`) and its name from
|
|
254
|
+
* the global configuration `plugins` array. Idempotent: an absent package only cleans a stale
|
|
255
|
+
* config entry. The config is untouched when npm fails.
|
|
256
|
+
*/
|
|
257
|
+
export async function uninstallPlugin(input) {
|
|
258
|
+
const parsed = parsePluginSpec(input.packageName);
|
|
259
|
+
if (parsed.version !== undefined || input.packageName.trim().startsWith("npm:"))
|
|
260
|
+
throw new Error(`Invalid package name "${input.packageName}": pass the bare npm package name, without "npm:" or a version`);
|
|
261
|
+
if (input.readOnly)
|
|
262
|
+
throw new Error(`Uninstalling npm plugin "${parsed.name}" is unavailable under --read-only: it runs npm and writes your global configuration. Run without --read-only.`);
|
|
263
|
+
const home = resolve(input.configHome ?? configHome());
|
|
264
|
+
const pluginsDir = join(home, "plugins");
|
|
265
|
+
const packageDir = join(pluginsDir, "node_modules", parsed.name);
|
|
266
|
+
const manifest = (await readJson(join(packageDir, "package.json")));
|
|
267
|
+
const present = manifest !== undefined;
|
|
268
|
+
const dependency = await pluginDependencySpec(pluginsDir, parsed.name);
|
|
269
|
+
const stored = storedTarballOf(pluginsDir, dependency);
|
|
270
|
+
if (present || dependency !== undefined) {
|
|
271
|
+
const signal = input.signal ?? AbortSignal.timeout(INSTALL_TIMEOUT_MS);
|
|
272
|
+
const run = input.runner ?? defaultInstallRunner;
|
|
273
|
+
const result = await run("npm", ["uninstall", "--prefix", pluginsDir, parsed.name], {
|
|
274
|
+
cwd: pluginsDir,
|
|
275
|
+
signal,
|
|
276
|
+
});
|
|
277
|
+
if (result.exitCode !== 0) {
|
|
278
|
+
const detail = sanitizeNpmError([result.stdout, result.stderr].filter(Boolean).join("\n"));
|
|
279
|
+
throw new Error(`npm uninstall failed for plugin "${parsed.name}" (exit ${result.exitCode}).\n${detail || "no output"}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const { file, changed } = await updateGlobalPluginEntries(home, (current) => current.filter((entry) => entry !== parsed.name), { createIfMissing: false });
|
|
283
|
+
// The copy kept for a local-file install goes with the package.
|
|
284
|
+
if (stored)
|
|
285
|
+
await rm(stored, { force: true });
|
|
286
|
+
return {
|
|
287
|
+
packageName: parsed.name,
|
|
288
|
+
removed: present || changed || stored !== undefined,
|
|
289
|
+
pluginsDir,
|
|
290
|
+
configFile: file,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
203
293
|
/**
|
|
204
294
|
* Refreshes every plugin package installed in the global plugins directory in ONE
|
|
205
295
|
* `npm install --prefix <plugins> <pkg>@latest ...` call, which lets npm resolve all peer ranges
|
|
@@ -214,7 +304,7 @@ export async function updateAllPlugins(input) {
|
|
|
214
304
|
if (names.length === 0)
|
|
215
305
|
return { packages: [], pluginsDir };
|
|
216
306
|
const signal = input.signal ?? AbortSignal.timeout(INSTALL_TIMEOUT_MS);
|
|
217
|
-
const run = input.runner ??
|
|
307
|
+
const run = input.runner ?? defaultInstallRunner;
|
|
218
308
|
const targets = names.map((name) => npmInstallTarget(name, undefined, true));
|
|
219
309
|
const npmResult = await run("npm", ["install", "--prefix", pluginsDir, ...targets], {
|
|
220
310
|
cwd: pluginsDir,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type InstallRunner } from "./install.ts";
|
|
2
|
+
import { type PluginTarballInfo } from "./tarball.ts";
|
|
3
|
+
export type LocalInstallPhase = "validating" | "copying" | "installing" | "registering";
|
|
4
|
+
export interface InstallLocalPluginInput {
|
|
5
|
+
tarballPath: string;
|
|
6
|
+
configHome?: string;
|
|
7
|
+
readOnly?: boolean;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
runner?: InstallRunner;
|
|
10
|
+
/** Called as each phase starts. */
|
|
11
|
+
onPhase?: (phase: LocalInstallPhase) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface InstallLocalPluginResult {
|
|
14
|
+
packageName: string;
|
|
15
|
+
installedVersion: string;
|
|
16
|
+
previousVersion?: string;
|
|
17
|
+
/** A version of the package was installed before this call. */
|
|
18
|
+
alreadyInstalled: boolean;
|
|
19
|
+
updated: boolean;
|
|
20
|
+
storedPath: string;
|
|
21
|
+
/** The dependency spec npm records, relative to the plugins directory. */
|
|
22
|
+
spec: string;
|
|
23
|
+
pluginsDir: string;
|
|
24
|
+
configFile: string;
|
|
25
|
+
info: PluginTarballInfo;
|
|
26
|
+
}
|
|
27
|
+
/** An install failure that carries the tail of npm's output for the UI. */
|
|
28
|
+
export declare class LocalInstallError extends Error {
|
|
29
|
+
readonly logTail?: string | undefined;
|
|
30
|
+
constructor(message: string, logTail?: string | undefined);
|
|
31
|
+
}
|
|
32
|
+
export declare const storedTarballName: (name: string, version: string) => string;
|
|
33
|
+
export declare function installLocalPluginTarball(input: InstallLocalPluginInput): Promise<InstallLocalPluginResult>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installs a plugin from a local `.tgz` (`npm pack` / `pnpm pack`): private plugins that are never
|
|
3
|
+
* published. The validated tarball is copied into `<pluginsDir>/local-packages/` and npm installs
|
|
4
|
+
* FROM THAT COPY, so the `file:` dependency npm records in `<pluginsDir>/package.json` is stable
|
|
5
|
+
* and self-contained: moving or deleting the original file cannot break a later install of
|
|
6
|
+
* another plugin. Any failure rolls back (stored copy, partial package, config entry).
|
|
7
|
+
*/
|
|
8
|
+
import { chmod, copyFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { configHome } from "../config.js";
|
|
11
|
+
import { readJson } from "../runtime/fs.js";
|
|
12
|
+
import { defaultInstallRunner, INSTALL_TIMEOUT_MS, LOCAL_PACKAGES_DIR, persistGlobalPluginEntry, pluginDependencySpec, removeGlobalPluginEntry, sanitizeNpmError, storedTarballOf, } from "./install.js";
|
|
13
|
+
import { inspectPluginTarball } from "./tarball.js";
|
|
14
|
+
/** An install failure that carries the tail of npm's output for the UI. */
|
|
15
|
+
export class LocalInstallError extends Error {
|
|
16
|
+
logTail;
|
|
17
|
+
constructor(message, logTail) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.logTail = logTail;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const TAIL_CHARS = 1500;
|
|
23
|
+
const safePart = (text) => text.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
24
|
+
export const storedTarballName = (name, version) => `${safePart(name.replace(/^@/, "").replace(/\//g, "-"))}-${safePart(version)}.tgz`;
|
|
25
|
+
export async function installLocalPluginTarball(input) {
|
|
26
|
+
if (input.readOnly)
|
|
27
|
+
throw new Error("Installing a plugin from a file is unavailable under --read-only: it runs npm and writes your global configuration. Run without --read-only.");
|
|
28
|
+
input.onPhase?.("validating");
|
|
29
|
+
const info = await inspectPluginTarball(input.tarballPath);
|
|
30
|
+
if (!info.isAlisioPlugin || !info.name || !info.version)
|
|
31
|
+
throw new Error(`This file is not a valid Alisio plugin package: ${info.reasons.join("; ")}`);
|
|
32
|
+
const name = info.name;
|
|
33
|
+
const version = info.version;
|
|
34
|
+
const home = resolve(input.configHome ?? configHome());
|
|
35
|
+
const pluginsDir = join(home, "plugins");
|
|
36
|
+
const localDir = join(pluginsDir, LOCAL_PACKAGES_DIR);
|
|
37
|
+
const packageDir = join(pluginsDir, "node_modules", name);
|
|
38
|
+
const run = input.runner ?? defaultInstallRunner;
|
|
39
|
+
const signal = input.signal ?? AbortSignal.timeout(INSTALL_TIMEOUT_MS);
|
|
40
|
+
const previous = (await readJson(join(packageDir, "package.json")));
|
|
41
|
+
const previousSpec = await pluginDependencySpec(pluginsDir, name);
|
|
42
|
+
const previousStored = storedTarballOf(pluginsDir, previousSpec);
|
|
43
|
+
// What to reinstall from if an update fails half-way: our stored copy, or the external file a
|
|
44
|
+
// plugin installed by hand still points at.
|
|
45
|
+
const previousSource = previousStored ??
|
|
46
|
+
(previousSpec?.startsWith("file:") ? resolve(pluginsDir, previousSpec.slice(5)) : undefined);
|
|
47
|
+
const snapshot = async (file) => readFile(file, "utf8").catch(() => undefined);
|
|
48
|
+
const manifestFiles = ["package.json", "package-lock.json"].map((f) => join(pluginsDir, f));
|
|
49
|
+
const snapshots = await Promise.all(manifestFiles.map(snapshot));
|
|
50
|
+
const configBefore = (await readJson(join(home, "config.json")));
|
|
51
|
+
const inConfig = Array.isArray(configBefore?.plugins) && configBefore.plugins.includes(name);
|
|
52
|
+
input.onPhase?.("copying");
|
|
53
|
+
await mkdir(localDir, { recursive: true, mode: 0o700 });
|
|
54
|
+
const storedPath = join(localDir, storedTarballName(name, version));
|
|
55
|
+
const backup = `${storedPath}.bak`;
|
|
56
|
+
const hadStored = await rename(storedPath, backup).then(() => true, () => false);
|
|
57
|
+
try {
|
|
58
|
+
await copyFile(input.tarballPath, storedPath);
|
|
59
|
+
await chmod(storedPath, 0o600);
|
|
60
|
+
input.onPhase?.("installing");
|
|
61
|
+
const result = await run("npm", ["install", "--prefix", pluginsDir, storedPath], {
|
|
62
|
+
cwd: pluginsDir,
|
|
63
|
+
signal,
|
|
64
|
+
});
|
|
65
|
+
if (result.exitCode !== 0) {
|
|
66
|
+
const tail = sanitizeNpmError([result.stdout, result.stderr].filter(Boolean).join("\n"), 100_000).slice(-TAIL_CHARS);
|
|
67
|
+
throw new LocalInstallError(`npm install failed for plugin "${name}" (exit ${result.exitCode}).\n${tail || "no output"}`, tail || undefined);
|
|
68
|
+
}
|
|
69
|
+
input.onPhase?.("registering");
|
|
70
|
+
const configFile = await persistGlobalPluginEntry(home, name);
|
|
71
|
+
await rm(backup, { force: true });
|
|
72
|
+
if (previousStored && previousStored !== storedPath)
|
|
73
|
+
await rm(previousStored, { force: true });
|
|
74
|
+
const manifest = (await readJson(join(packageDir, "package.json")));
|
|
75
|
+
return {
|
|
76
|
+
packageName: name,
|
|
77
|
+
installedVersion: manifest?.version ?? version,
|
|
78
|
+
...(previous?.version ? { previousVersion: previous.version } : {}),
|
|
79
|
+
alreadyInstalled: previous !== undefined,
|
|
80
|
+
updated: previous !== undefined,
|
|
81
|
+
storedPath,
|
|
82
|
+
spec: `file:${LOCAL_PACKAGES_DIR}/${storedTarballName(name, version)}`,
|
|
83
|
+
pluginsDir,
|
|
84
|
+
configFile,
|
|
85
|
+
info,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
await rm(storedPath, { force: true }).catch(() => { });
|
|
90
|
+
if (hadStored)
|
|
91
|
+
await rename(backup, storedPath).catch(() => { });
|
|
92
|
+
if (previous !== undefined && previousSource) {
|
|
93
|
+
// An update that broke the package: put the previous version back.
|
|
94
|
+
const current = (await readJson(join(packageDir, "package.json")).catch(() => undefined));
|
|
95
|
+
if (current?.version !== previous.version)
|
|
96
|
+
await run("npm", ["install", "--prefix", pluginsDir, previousSource], {
|
|
97
|
+
cwd: pluginsDir,
|
|
98
|
+
signal: AbortSignal.timeout(INSTALL_TIMEOUT_MS),
|
|
99
|
+
}).catch(() => undefined);
|
|
100
|
+
}
|
|
101
|
+
// npm rewrites the dependency spec; a spec pointing at the copy we just removed would break
|
|
102
|
+
// every later install.
|
|
103
|
+
await Promise.all(manifestFiles.map((file, i) => {
|
|
104
|
+
const before = snapshots[i];
|
|
105
|
+
return before === undefined ? rm(file, { force: true }) : writeFile(file, before);
|
|
106
|
+
})).catch(() => { });
|
|
107
|
+
if (previous === undefined) {
|
|
108
|
+
// A package that was not there before must not be left half-installed.
|
|
109
|
+
if (await readJson(join(packageDir, "package.json")).catch(() => undefined)) {
|
|
110
|
+
await run("npm", ["uninstall", "--prefix", pluginsDir, name], {
|
|
111
|
+
cwd: pluginsDir,
|
|
112
|
+
signal: AbortSignal.timeout(60_000),
|
|
113
|
+
}).catch(() => undefined);
|
|
114
|
+
await rm(packageDir, { recursive: true, force: true }).catch(() => { });
|
|
115
|
+
}
|
|
116
|
+
if (!inConfig)
|
|
117
|
+
await removeGlobalPluginEntry(home, name).catch(() => false);
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How Alisio spawns npm for plugin install and uninstall.
|
|
3
|
+
*
|
|
4
|
+
* A system npm is the default. The desktop app ships without Node.js on the user's PATH, so it
|
|
5
|
+
* bundles npm and points `ALISIO_NPM_CLI` at its `bin/npm-cli.js`: npm then runs through the
|
|
6
|
+
* executable that is already running (Node, or the Electron binary acting as Node), with no
|
|
7
|
+
* system npm needed.
|
|
8
|
+
*/
|
|
9
|
+
import { type ProcessResult } from "../runtime/process.ts";
|
|
10
|
+
export declare const NPM_NOT_FOUND_MESSAGE = "npm was not found: install Node.js, or use the desktop app, which bundles it";
|
|
11
|
+
export interface NpmInvocation {
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
/** Present only when the child needs an explicit environment (bundled npm). */
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Maps `npm <args>` to the process to spawn. With `ALISIO_NPM_CLI` set, that script runs through
|
|
19
|
+
* `execPath`, and `ELECTRON_RUN_AS_NODE=1` is added when that executable is Electron.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveNpmInvocation(command: string, args: string[], env?: NodeJS.ProcessEnv, versions?: Record<string, string | undefined>, execPath?: string): NpmInvocation;
|
|
22
|
+
/** The default install runner: bounded time and output, bundled npm aware, clear ENOENT error. */
|
|
23
|
+
export declare function runNpm(command: string, args: string[], options: {
|
|
24
|
+
cwd: string;
|
|
25
|
+
signal: AbortSignal;
|
|
26
|
+
env?: Record<string, string>;
|
|
27
|
+
}, limits?: {
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
}): Promise<ProcessResult>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How Alisio spawns npm for plugin install and uninstall.
|
|
3
|
+
*
|
|
4
|
+
* A system npm is the default. The desktop app ships without Node.js on the user's PATH, so it
|
|
5
|
+
* bundles npm and points `ALISIO_NPM_CLI` at its `bin/npm-cli.js`: npm then runs through the
|
|
6
|
+
* executable that is already running (Node, or the Electron binary acting as Node), with no
|
|
7
|
+
* system npm needed.
|
|
8
|
+
*/
|
|
9
|
+
import { isMissingCommand, runProcess } from "../runtime/process.js";
|
|
10
|
+
export const NPM_NOT_FOUND_MESSAGE = "npm was not found: install Node.js, or use the desktop app, which bundles it";
|
|
11
|
+
/** Env keys the npm child inherits (the default runner env plus what npm needs for its cache/proxy). */
|
|
12
|
+
const NPM_ENV_KEYS = [
|
|
13
|
+
"PATH",
|
|
14
|
+
"HOME",
|
|
15
|
+
"USERPROFILE",
|
|
16
|
+
"TMPDIR",
|
|
17
|
+
"TEMP",
|
|
18
|
+
"TMP",
|
|
19
|
+
"SystemRoot",
|
|
20
|
+
"COMSPEC",
|
|
21
|
+
"LANG",
|
|
22
|
+
"APPDATA",
|
|
23
|
+
"LOCALAPPDATA",
|
|
24
|
+
"HTTP_PROXY",
|
|
25
|
+
"HTTPS_PROXY",
|
|
26
|
+
"NO_PROXY",
|
|
27
|
+
"http_proxy",
|
|
28
|
+
"https_proxy",
|
|
29
|
+
"no_proxy",
|
|
30
|
+
"NODE_EXTRA_CA_CERTS",
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Maps `npm <args>` to the process to spawn. With `ALISIO_NPM_CLI` set, that script runs through
|
|
34
|
+
* `execPath`, and `ELECTRON_RUN_AS_NODE=1` is added when that executable is Electron.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveNpmInvocation(command, args, env = process.env, versions = process.versions, execPath = process.execPath) {
|
|
37
|
+
const cli = env.ALISIO_NPM_CLI;
|
|
38
|
+
if (command !== "npm" || !cli)
|
|
39
|
+
return { command, args };
|
|
40
|
+
const childEnv = {};
|
|
41
|
+
for (const key of NPM_ENV_KEYS) {
|
|
42
|
+
const value = env[key];
|
|
43
|
+
if (value)
|
|
44
|
+
childEnv[key] = value;
|
|
45
|
+
}
|
|
46
|
+
if (versions.electron)
|
|
47
|
+
childEnv.ELECTRON_RUN_AS_NODE = "1";
|
|
48
|
+
return { command: execPath, args: [cli, ...args], env: childEnv };
|
|
49
|
+
}
|
|
50
|
+
/** The default install runner: bounded time and output, bundled npm aware, clear ENOENT error. */
|
|
51
|
+
export async function runNpm(command, args, options, limits = {}) {
|
|
52
|
+
const spec = resolveNpmInvocation(command, args);
|
|
53
|
+
try {
|
|
54
|
+
return await runProcess(spec.command, spec.args, {
|
|
55
|
+
cwd: options.cwd,
|
|
56
|
+
signal: options.signal,
|
|
57
|
+
...(spec.env || options.env ? { env: { ...spec.env, ...options.env } } : {}),
|
|
58
|
+
...limits,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (command === "npm" && isMissingCommand(error))
|
|
63
|
+
throw new Error(NPM_NOT_FOUND_MESSAGE);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|