@superblocksteam/vite-plugin-file-sync 2.0.123-next.0 → 2.0.124-next.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/ai-service/agent/prompts/build-base-system-prompt.d.ts +16 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js +27 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.js +3 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/api-comparator.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/api-comparator.js +1 -4
- package/dist/ai-service/agent/tools/apis/api-comparator.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/api-testing-state.js +7 -0
- package/dist/ai-service/agent/tools/apis/api-testing-state.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/get-api-docs.d.ts +1 -1
- package/dist/ai-service/agent/tools/apis/write-api.d.ts +2 -2
- package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-capture-screenshot.js +11 -33
- package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
- package/dist/ai-service/agent/tools/build-copy-directory.d.ts +1 -1
- package/dist/ai-service/agent/tools/build-finalize.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-finalize.js +11 -2
- package/dist/ai-service/agent/tools/build-finalize.js.map +1 -1
- package/dist/ai-service/agent/tools/build-install-packages.d.ts +41 -1
- package/dist/ai-service/agent/tools/build-install-packages.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-install-packages.js +208 -0
- package/dist/ai-service/agent/tools/build-install-packages.js.map +1 -1
- package/dist/ai-service/agent/tools/build-lookup-npm-package.d.ts +28 -0
- package/dist/ai-service/agent/tools/build-lookup-npm-package.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/build-lookup-npm-package.js +78 -0
- package/dist/ai-service/agent/tools/build-lookup-npm-package.js.map +1 -0
- package/dist/ai-service/agent/tools/build-manage-checklist.d.ts +4 -4
- package/dist/ai-service/agent/tools/build-navigate-preview.d.ts +16 -0
- package/dist/ai-service/agent/tools/build-navigate-preview.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/build-navigate-preview.js +68 -0
- package/dist/ai-service/agent/tools/build-navigate-preview.js.map +1 -0
- package/dist/ai-service/agent/tools/build-write-file.d.ts +1 -1
- package/dist/ai-service/agent/tools/databases/dev-database.d.ts +9 -9
- package/dist/ai-service/agent/tools/index.d.ts +2 -0
- package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/index.js +2 -0
- package/dist/ai-service/agent/tools/index.js.map +1 -1
- package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +3 -3
- package/dist/ai-service/agent/tools/report-security-findings.d.ts +11 -5
- package/dist/ai-service/agent/tools/report-security-findings.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/report-security-findings.js +1 -0
- package/dist/ai-service/agent/tools/report-security-findings.js.map +1 -1
- package/dist/ai-service/agent/tools.d.ts.map +1 -1
- package/dist/ai-service/agent/tools.js +3 -1
- package/dist/ai-service/agent/tools.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/git.d.ts +2 -2
- package/dist/ai-service/agent/tools2/tools/grep.d.ts +2 -2
- package/dist/ai-service/agent/tools2/tools/update-test-case-status.d.ts +2 -2
- package/dist/ai-service/app-interface/npm-error-parser.d.ts +161 -0
- package/dist/ai-service/app-interface/npm-error-parser.d.ts.map +1 -0
- package/dist/ai-service/app-interface/npm-error-parser.js +510 -0
- package/dist/ai-service/app-interface/npm-error-parser.js.map +1 -0
- package/dist/ai-service/app-interface/npm-package-lookup.d.ts +286 -0
- package/dist/ai-service/app-interface/npm-package-lookup.d.ts.map +1 -0
- package/dist/ai-service/app-interface/npm-package-lookup.js +793 -0
- package/dist/ai-service/app-interface/npm-package-lookup.js.map +1 -0
- package/dist/ai-service/app-interface/npm-registry.d.ts +735 -47
- package/dist/ai-service/app-interface/npm-registry.d.ts.map +1 -1
- package/dist/ai-service/app-interface/npm-registry.js +1882 -153
- package/dist/ai-service/app-interface/npm-registry.js.map +1 -1
- package/dist/ai-service/app-interface/shell.d.ts +118 -1
- package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
- package/dist/ai-service/app-interface/shell.js +579 -157
- package/dist/ai-service/app-interface/shell.js.map +1 -1
- package/dist/ai-service/app-skills/manager.d.ts +1 -1
- package/dist/ai-service/app-skills/manager.d.ts.map +1 -1
- package/dist/ai-service/app-skills/manager.js +39 -7
- package/dist/ai-service/app-skills/manager.js.map +1 -1
- package/dist/ai-service/checklist/api-migration-checklist-gate.d.ts +9 -0
- package/dist/ai-service/checklist/api-migration-checklist-gate.d.ts.map +1 -0
- package/dist/ai-service/checklist/api-migration-checklist-gate.js +30 -0
- package/dist/ai-service/checklist/api-migration-checklist-gate.js.map +1 -0
- package/dist/ai-service/checklist/api-migration-origins.d.ts +4 -0
- package/dist/ai-service/checklist/api-migration-origins.d.ts.map +1 -0
- package/dist/ai-service/checklist/api-migration-origins.js +8 -0
- package/dist/ai-service/checklist/api-migration-origins.js.map +1 -0
- package/dist/ai-service/checklist/persisted-checklist-store.d.ts +2 -3
- package/dist/ai-service/checklist/persisted-checklist-store.d.ts.map +1 -1
- package/dist/ai-service/checklist/persisted-checklist-store.js +6 -7
- package/dist/ai-service/checklist/persisted-checklist-store.js.map +1 -1
- package/dist/ai-service/context-archive-paths.d.ts +4 -0
- package/dist/ai-service/context-archive-paths.d.ts.map +1 -0
- package/dist/ai-service/context-archive-paths.js +28 -0
- package/dist/ai-service/context-archive-paths.js.map +1 -0
- package/dist/ai-service/context-download.d.ts +3 -1
- package/dist/ai-service/context-download.d.ts.map +1 -1
- package/dist/ai-service/context-download.js +48 -9
- package/dist/ai-service/context-download.js.map +1 -1
- package/dist/ai-service/context-upload.d.ts +3 -2
- package/dist/ai-service/context-upload.d.ts.map +1 -1
- package/dist/ai-service/context-upload.js +44 -26
- package/dist/ai-service/context-upload.js.map +1 -1
- package/dist/ai-service/dev-database-client.d.ts +3 -11
- package/dist/ai-service/dev-database-client.d.ts.map +1 -1
- package/dist/ai-service/dev-database-client.js.map +1 -1
- package/dist/ai-service/features.d.ts +4 -0
- package/dist/ai-service/features.d.ts.map +1 -1
- package/dist/ai-service/features.js +8 -0
- package/dist/ai-service/features.js.map +1 -1
- package/dist/ai-service/filter-disabled-tools-for-migration.d.ts.map +1 -1
- package/dist/ai-service/filter-disabled-tools-for-migration.js +4 -0
- package/dist/ai-service/filter-disabled-tools-for-migration.js.map +1 -1
- package/dist/ai-service/index.d.ts +94 -0
- package/dist/ai-service/index.d.ts.map +1 -1
- package/dist/ai-service/index.js +564 -17
- package/dist/ai-service/index.js.map +1 -1
- package/dist/ai-service/llm/client.d.ts +38 -3
- package/dist/ai-service/llm/client.d.ts.map +1 -1
- package/dist/ai-service/llm/client.js +73 -22
- package/dist/ai-service/llm/client.js.map +1 -1
- package/dist/ai-service/llm/context-v2/adapter.d.ts +3 -1
- package/dist/ai-service/llm/context-v2/adapter.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/adapter.js +11 -11
- package/dist/ai-service/llm/context-v2/adapter.js.map +1 -1
- package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts +44 -0
- package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/client-side.js +170 -0
- package/dist/ai-service/llm/context-v2/compaction/client-side.js.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.d.ts +56 -0
- package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.js +9 -0
- package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.js.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/index.d.ts +5 -0
- package/dist/ai-service/llm/context-v2/compaction/index.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/index.js +3 -0
- package/dist/ai-service/llm/context-v2/compaction/index.js.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts +30 -0
- package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/server-side.js +130 -0
- package/dist/ai-service/llm/context-v2/compaction/server-side.js.map +1 -0
- package/dist/ai-service/llm/context-v2/context-management.d.ts +203 -0
- package/dist/ai-service/llm/context-v2/context-management.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/context-management.js +183 -0
- package/dist/ai-service/llm/context-v2/context-management.js.map +1 -0
- package/dist/ai-service/llm/context-v2/context.d.ts +64 -22
- package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context.js +233 -157
- package/dist/ai-service/llm/context-v2/context.js.map +1 -1
- package/dist/ai-service/llm/context-v2/conversation-context.d.ts +24 -7
- package/dist/ai-service/llm/context-v2/conversation-context.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/conversation-context.js +1 -1
- package/dist/ai-service/llm/context-v2/index.d.ts +0 -4
- package/dist/ai-service/llm/context-v2/index.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/index.js +0 -4
- package/dist/ai-service/llm/context-v2/index.js.map +1 -1
- package/dist/ai-service/llm/context-v2/manager.d.ts +24 -3
- package/dist/ai-service/llm/context-v2/manager.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/manager.js +146 -4
- package/dist/ai-service/llm/context-v2/manager.js.map +1 -1
- package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.d.ts +2 -7
- package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.js +5 -73
- package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.js.map +1 -1
- package/dist/ai-service/llm/context-v2/storage/event-store.d.ts +57 -0
- package/dist/ai-service/llm/context-v2/storage/event-store.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/event-store.js +10 -0
- package/dist/ai-service/llm/context-v2/storage/event-store.js.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/event-types.d.ts +50 -0
- package/dist/ai-service/llm/context-v2/storage/event-types.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/event-types.js +10 -0
- package/dist/ai-service/llm/context-v2/storage/event-types.js.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.d.ts +87 -0
- package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.js +184 -0
- package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.js.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/migration.d.ts +49 -0
- package/dist/ai-service/llm/context-v2/storage/migration.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/migration.js +126 -0
- package/dist/ai-service/llm/context-v2/storage/migration.js.map +1 -0
- package/dist/ai-service/llm/context-v2/types.d.ts +16 -11
- package/dist/ai-service/llm/context-v2/types.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/types.js +2 -2
- package/dist/ai-service/llm/context-v2/types.js.map +1 -1
- package/dist/ai-service/llm/impl/clark.d.ts +8 -0
- package/dist/ai-service/llm/impl/clark.d.ts.map +1 -1
- package/dist/ai-service/llm/impl/clark.js +8 -0
- package/dist/ai-service/llm/impl/clark.js.map +1 -1
- package/dist/ai-service/llm/interaction/adapters/vercel.d.ts.map +1 -1
- package/dist/ai-service/llm/interaction/adapters/vercel.js +17 -1
- package/dist/ai-service/llm/interaction/adapters/vercel.js.map +1 -1
- package/dist/ai-service/llm/provider.d.ts.map +1 -1
- package/dist/ai-service/llm/provider.js +1 -3
- package/dist/ai-service/llm/provider.js.map +1 -1
- package/dist/ai-service/llm/stream/observers/context.d.ts +23 -0
- package/dist/ai-service/llm/stream/observers/context.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/observers/context.js +62 -0
- package/dist/ai-service/llm/stream/observers/context.js.map +1 -1
- package/dist/ai-service/llm/tool-context-integrity.d.ts +25 -0
- package/dist/ai-service/llm/tool-context-integrity.d.ts.map +1 -0
- package/dist/ai-service/llm/tool-context-integrity.js +141 -0
- package/dist/ai-service/llm/tool-context-integrity.js.map +1 -0
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js +6 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js.map +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.js +3 -2
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.js.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts +23 -2
- package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.js +37 -2
- package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.js +29 -2
- package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/awaiting-user.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/awaiting-user.js +9 -0
- package/dist/ai-service/state-machine/handlers/awaiting-user.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.js +25 -2
- package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.d.ts +6 -0
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.d.ts.map +1 -0
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js +36 -0
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js.map +1 -0
- package/dist/ai-service/state-machine/helpers/stable-peer.d.ts +30 -1
- package/dist/ai-service/state-machine/helpers/stable-peer.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/stable-peer.js +85 -2
- package/dist/ai-service/state-machine/helpers/stable-peer.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/user-preferences.d.ts +8 -0
- package/dist/ai-service/state-machine/helpers/user-preferences.d.ts.map +1 -0
- package/dist/ai-service/state-machine/helpers/user-preferences.js +11 -0
- package/dist/ai-service/state-machine/helpers/user-preferences.js.map +1 -0
- package/dist/ai-service/template-renderer.d.ts +18 -1
- package/dist/ai-service/template-renderer.d.ts.map +1 -1
- package/dist/ai-service/template-renderer.js +73 -12
- package/dist/ai-service/template-renderer.js.map +1 -1
- package/dist/ai-service/types.d.ts +157 -0
- package/dist/ai-service/types.d.ts.map +1 -1
- package/dist/ai-service/types.js +137 -0
- package/dist/ai-service/types.js.map +1 -1
- package/dist/ai-service/util/llm-config-utils.d.ts +21 -22
- package/dist/ai-service/util/llm-config-utils.d.ts.map +1 -1
- package/dist/ai-service/util/llm-config-utils.js +40 -67
- package/dist/ai-service/util/llm-config-utils.js.map +1 -1
- package/dist/file-sync-vite-plugin.d.ts.map +1 -1
- package/dist/file-sync-vite-plugin.js +5 -0
- package/dist/file-sync-vite-plugin.js.map +1 -1
- package/dist/file-system-helpers.d.ts +17 -0
- package/dist/file-system-helpers.d.ts.map +1 -1
- package/dist/file-system-helpers.js +22 -0
- package/dist/file-system-helpers.js.map +1 -1
- package/dist/git-service/index.d.ts.map +1 -1
- package/dist/git-service/index.js +15 -1
- package/dist/git-service/index.js.map +1 -1
- package/dist/migration/migration-routes.d.ts.map +1 -1
- package/dist/migration/migration-routes.js +26 -3
- package/dist/migration/migration-routes.js.map +1 -1
- package/dist/migration/translation-prompt.d.ts.map +1 -1
- package/dist/migration/translation-prompt.js +5 -0
- package/dist/migration/translation-prompt.js.map +1 -1
- package/dist/migration-templates/app-fullstack/package.json +2 -2
- package/dist/policy-gate-callback-mapper.d.ts +24 -0
- package/dist/policy-gate-callback-mapper.d.ts.map +1 -0
- package/dist/policy-gate-callback-mapper.js +61 -0
- package/dist/policy-gate-callback-mapper.js.map +1 -0
- package/dist/policy-gate-runner.d.ts +32 -0
- package/dist/policy-gate-runner.d.ts.map +1 -0
- package/dist/policy-gate-runner.js +191 -0
- package/dist/policy-gate-runner.js.map +1 -0
- package/dist/router-parser.d.ts.map +1 -1
- package/dist/router-parser.js +107 -11
- package/dist/router-parser.js.map +1 -1
- package/dist/sync-service/list-dir.d.ts.map +1 -1
- package/dist/sync-service/list-dir.js +13 -3
- package/dist/sync-service/list-dir.js.map +1 -1
- package/package.json +28 -10
|
@@ -39,14 +39,52 @@ import { promisify } from "node:util";
|
|
|
39
39
|
import { glob } from "glob";
|
|
40
40
|
import { detect, resolveCommand } from "package-manager-detector";
|
|
41
41
|
import { normalizePath } from "vite";
|
|
42
|
+
import { bucketNpmRegistryHost, recordNpmInstall, } from "@superblocksteam/telemetry";
|
|
42
43
|
import { getErrorMeta, getLogger } from "../../util/logger.js";
|
|
43
44
|
import llmobs from "../llmobs/index.js";
|
|
44
|
-
import { NpmInstallError } from "../types.js";
|
|
45
|
+
import { NpmInstallBlocked, NpmInstallError, scrubSecrets, } from "../types.js";
|
|
45
46
|
import { CLARK_APP_BLACKLIST_PATTERNS, APPLICATION_FILE_PATTERNS, STANDARD_IGNORE_PATTERNS, } from "./constants.js";
|
|
46
47
|
import { DraftManager, validateFileOperation, } from "./filesystem/index.js";
|
|
47
48
|
import { Linter } from "./linter.js";
|
|
48
|
-
import {
|
|
49
|
+
import { parseNpmJsonDiagnostic, parseNpmJsonError, parseNpmJsonSuccess, } from "./npm-error-parser.js";
|
|
50
|
+
import { prepareForPrivateRegistry, PRESERVE_NPMRC_SCOPES, shouldIgnoreInstallScripts, } from "./npm-registry.js";
|
|
49
51
|
const execPromise = promisify(exec);
|
|
52
|
+
// `npm install --json` emits the full install summary on stdout. The default
|
|
53
|
+
// 1 MiB exec buffer can be exceeded for fat dependency trees, which would
|
|
54
|
+
// fail a successful install with ERR_CHILD_PROCESS_STDIO_MAXBUFFER and reach
|
|
55
|
+
// `classifyNpmExecError` with truncated stdout — defeating the `--json`
|
|
56
|
+
// error-envelope contract these entry points depend on. 4 MiB comfortably
|
|
57
|
+
// covers real-world npm summaries (per-package entries are tiny) while
|
|
58
|
+
// keeping the worst-case buffer + parsed-object peak bounded.
|
|
59
|
+
const NPM_JSON_MAX_BUFFER = 4 * 1024 * 1024;
|
|
60
|
+
/**
|
|
61
|
+
* Map an install failure to a value in the closed `npm.outcome` enum
|
|
62
|
+
* (APPS-4189). `NpmInstallBlocked` carries the structured reason from
|
|
63
|
+
* P1.2; anything else is bucketed as `other` at the metric boundary.
|
|
64
|
+
* `normalizeNpmOutcome` enforces the allowlist a second time downstream
|
|
65
|
+
* so a stray reason value cannot leak past the emit boundary.
|
|
66
|
+
*/
|
|
67
|
+
function outcomeFromInstallError(err) {
|
|
68
|
+
if (err instanceof NpmInstallBlocked)
|
|
69
|
+
return err.reason;
|
|
70
|
+
return "other";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Pick the registry URL the install actually targeted from the most-recent
|
|
74
|
+
* `prepareForPrivateRegistry` / `maybeWriteNpmrcForDir` result. Returns
|
|
75
|
+
* `undefined` (→ `registry_host=unknown`) when no configured row was in
|
|
76
|
+
* effect, including the `unreachable` and `not-configured` cases — those
|
|
77
|
+
* are deliberately reported as `unknown` rather than guessed at public
|
|
78
|
+
* npm, so the dashboard signal "did private-registry routing actually
|
|
79
|
+
* fire?" stays honest.
|
|
80
|
+
*/
|
|
81
|
+
function registryUrlFromFetchResult(result) {
|
|
82
|
+
if (!result)
|
|
83
|
+
return undefined;
|
|
84
|
+
if (!result.config.configured)
|
|
85
|
+
return undefined;
|
|
86
|
+
return result.config.default?.url;
|
|
87
|
+
}
|
|
50
88
|
const DEFAULT_COPY_DIRECTORY_EXCLUDES = new Set([
|
|
51
89
|
".git",
|
|
52
90
|
".next",
|
|
@@ -135,12 +173,22 @@ let AppShell = (() => {
|
|
|
135
173
|
virtualFs;
|
|
136
174
|
pathValidator;
|
|
137
175
|
enableDeletingLockfiles;
|
|
176
|
+
/**
|
|
177
|
+
* Resolves the per-org npm registry config at install time. The AiService
|
|
178
|
+
* always constructs one in production (SaaS, local dev, and ephemeral
|
|
179
|
+
* pods); the LD flag (`superblocks.npm-registry.enabled`) inside the
|
|
180
|
+
* client decides whether to short-circuit to "not-configured" without a
|
|
181
|
+
* server call. `undefined` here is the test-only path (or callers that
|
|
182
|
+
* deliberately opt out): every `.npmrc` helper becomes a no-op and the
|
|
183
|
+
* install runs against npm's default resolution.
|
|
184
|
+
*/
|
|
185
|
+
npmRegistryClient;
|
|
138
186
|
/**
|
|
139
187
|
* Tracks files that must be read before they can be written.
|
|
140
188
|
* Used to enforce read-before-write pattern for AI safety.
|
|
141
189
|
*/
|
|
142
190
|
requiredReads = new Set();
|
|
143
|
-
constructor({ appRootDirPath, fsOperationQueue, messageSender, draftInterface, templateRenderer, virtualFileSystem, pathValidator, enableDeletingLockfiles, }) {
|
|
191
|
+
constructor({ appRootDirPath, fsOperationQueue, messageSender, draftInterface, templateRenderer, virtualFileSystem, pathValidator, enableDeletingLockfiles, npmRegistryClient, }) {
|
|
144
192
|
// Normalize path to handle Windows drive letter case
|
|
145
193
|
this.appRootDirPath = normalizePath(appRootDirPath);
|
|
146
194
|
this.fsOperationQueue = fsOperationQueue;
|
|
@@ -153,6 +201,7 @@ let AppShell = (() => {
|
|
|
153
201
|
this.virtualFs = virtualFileSystem;
|
|
154
202
|
this.pathValidator = pathValidator;
|
|
155
203
|
this.enableDeletingLockfiles = enableDeletingLockfiles ?? false;
|
|
204
|
+
this.npmRegistryClient = npmRegistryClient;
|
|
156
205
|
}
|
|
157
206
|
/**
|
|
158
207
|
* Toggle the API virtual-file projection. When disabled, reads to `apis/*`
|
|
@@ -276,182 +325,555 @@ let AppShell = (() => {
|
|
|
276
325
|
warmLinterCache() {
|
|
277
326
|
void this.linter.warmCache(["**/*.{tsx}"]);
|
|
278
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Emit one `superblocks.npm.install.*` observation (APPS-4189 / P6.2) and
|
|
330
|
+
* annotate the active llmobs span with the same dimensions. Shared by
|
|
331
|
+
* every `AppShell.*install*` method so metric and span streams join on
|
|
332
|
+
* (runner, registry_host, outcome) without drift. Every value flows
|
|
333
|
+
* through the P6.1 sanitization helpers inside `recordNpmInstall`, so a
|
|
334
|
+
* raw registry hostname or token cannot reach an exported attribute even
|
|
335
|
+
* if an upstream caller forgot to scrub.
|
|
336
|
+
*/
|
|
337
|
+
observeNpmInstall(runner, registryFetchResult, outcome, durationMs) {
|
|
338
|
+
const tags = recordNpmInstall({
|
|
339
|
+
runner,
|
|
340
|
+
registryHost: registryUrlFromFetchResult(registryFetchResult),
|
|
341
|
+
outcome,
|
|
342
|
+
durationMs,
|
|
343
|
+
});
|
|
344
|
+
llmobs.annotate({ tags });
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Env overlay for AppShell-spawned npm/pnpm installs in the live-edit
|
|
348
|
+
* pod. Routes npm's per-run debug log into `<app>/.superblocks/logs` via
|
|
349
|
+
* `NPM_CONFIG_LOGS_DIR` so a failed agent install leaves a collectable log
|
|
350
|
+
* in a predictable place — the same convention the CLI dev-server install
|
|
351
|
+
* uses (`buildInstallEnv` in the SDK). These execs otherwise inherit
|
|
352
|
+
* `process.env` implicitly, so we spread it before adding the override.
|
|
353
|
+
* pnpm has no equivalent logs-dir config and ignores the var (npm-only).
|
|
354
|
+
*/
|
|
355
|
+
installLogEnv() {
|
|
356
|
+
return {
|
|
357
|
+
...process.env,
|
|
358
|
+
NPM_CONFIG_LOGS_DIR: path.join(this.appRootDirPath, ".superblocks", "logs"),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
279
361
|
async runNpmInstall(abortSignal) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
362
|
+
const installStart = Date.now();
|
|
363
|
+
let outcome = "success";
|
|
364
|
+
let capturedFetchResult;
|
|
365
|
+
try {
|
|
366
|
+
// `withAuthFailedRetry` owns the `.npmrc` materialization (via
|
|
367
|
+
// `prepareForPrivateRegistry`) and the one-shot auth_failed retry —
|
|
368
|
+
// see its docstring. Telemetry is captured around it so a retry-then-
|
|
369
|
+
// succeed flow still records the right outcome.
|
|
370
|
+
await this.withAuthFailedRetry(async (registryFetchResult) => {
|
|
371
|
+
capturedFetchResult = registryFetchResult;
|
|
372
|
+
// APPS-4370: refuse to spawn npm when the registry server is
|
|
373
|
+
// unreachable AND we have prior evidence (disk marker) that this org
|
|
374
|
+
// was at some point configured with a private registry. See
|
|
375
|
+
// `maybeShortCircuitForUnreachable` for the full rationale.
|
|
376
|
+
this.maybeShortCircuitForUnreachable(registryFetchResult, []);
|
|
377
|
+
const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
|
|
378
|
+
// In csb mock server (local dev) we use pnpm to avoid lockfile / store
|
|
379
|
+
// conflicts with the surrounding workspace; everywhere else we force npm
|
|
380
|
+
// so the `--json` error contract — and therefore the registry-blocked
|
|
381
|
+
// advice matrix — applies on this post-processing path too. When the
|
|
382
|
+
// org has opted out via `allow_install_scripts=false`, append
|
|
383
|
+
// `--ignore-scripts` (the flag is honored by both npm and pnpm).
|
|
384
|
+
const isNpm = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
|
|
385
|
+
const baseCommand = isNpm
|
|
386
|
+
? "npm install --fund=false --audit=false --json"
|
|
387
|
+
: "pnpm install --config.confirmModulesPurge=false";
|
|
388
|
+
const command = ignoreScripts
|
|
389
|
+
? `${baseCommand} --ignore-scripts`
|
|
390
|
+
: baseCommand;
|
|
391
|
+
return await new Promise((resolve, reject) => {
|
|
392
|
+
exec(command, {
|
|
393
|
+
cwd: this.appRootDirPath,
|
|
394
|
+
env: this.installLogEnv(),
|
|
395
|
+
timeout: 30_000,
|
|
396
|
+
signal: abortSignal,
|
|
397
|
+
// See `NPM_JSON_MAX_BUFFER` — `--json` summary can outgrow the
|
|
398
|
+
// default 1 MiB exec buffer on fat dep trees.
|
|
399
|
+
maxBuffer: NPM_JSON_MAX_BUFFER,
|
|
400
|
+
}, (error, stdout, stderr) => {
|
|
401
|
+
if (error) {
|
|
402
|
+
// Route the npm path through the same classifier as the agent-tool
|
|
403
|
+
// install entry points so E404 / E401 / ENOTFOUND surface as a
|
|
404
|
+
// `NpmInstallBlocked` (with row-aware advice) instead of a raw
|
|
405
|
+
// `NpmInstallError(stderr)`. The pnpm path doesn't emit the
|
|
406
|
+
// `--json` envelope, so the classifier falls through to the
|
|
407
|
+
// legacy error for it — registry-blocked scenarios only matter
|
|
408
|
+
// in cloud-edit (which is the npm branch).
|
|
409
|
+
if (isNpm) {
|
|
410
|
+
return reject(this.classifyNpmExecError({ stdout, stderr, message: error.message }, undefined, registryFetchResult));
|
|
411
|
+
}
|
|
412
|
+
return reject(new NpmInstallError(stderr));
|
|
413
|
+
}
|
|
414
|
+
return resolve();
|
|
415
|
+
});
|
|
416
|
+
});
|
|
303
417
|
});
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
outcome = outcomeFromInstallError(err);
|
|
421
|
+
throw err;
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
this.observeNpmInstall("install_all", capturedFetchResult, outcome, Date.now() - installStart);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* APPS-4370. If the `prepareForPrivateRegistry` result tells us the
|
|
429
|
+
* server is unreachable AND the per-org disk marker says this org has
|
|
430
|
+
* at some prior point successfully resolved a configured registry,
|
|
431
|
+
* throw `NpmInstallBlocked(registry_unreachable)` BEFORE spawning npm.
|
|
432
|
+
*
|
|
433
|
+
* Without this short-circuit the install spawns anyway and hits one of
|
|
434
|
+
* two failure modes:
|
|
435
|
+
* 1. A previous install left a managed `.npmrc` with rotated credentials
|
|
436
|
+
* → npm 401s mid-deploy and `classifyNpmExecError` tags it
|
|
437
|
+
* `registry_auth_failed`, pointing the user at the wrong remediation.
|
|
438
|
+
* 2. No managed `.npmrc` exists → install silently resolves through
|
|
439
|
+
* public npm, defeating the privacy posture during the outage.
|
|
440
|
+
*
|
|
441
|
+
* Bypassed when:
|
|
442
|
+
* - The fetch result is anything other than `unreachable` (the
|
|
443
|
+
* existing code paths handle `configured` / `stale` / `not-configured`).
|
|
444
|
+
* - `everConfigured` is `undefined` (no store wired in — older call
|
|
445
|
+
* sites, test fixtures) or `false` (greenfield org / first install).
|
|
446
|
+
*
|
|
447
|
+
* `packages` flows onto the structured error so the agent loop can
|
|
448
|
+
* surface "we tried to install <these> and the registry was unreachable".
|
|
449
|
+
*/
|
|
450
|
+
maybeShortCircuitForUnreachable(registryFetchResult, packages) {
|
|
451
|
+
if (!registryFetchResult || registryFetchResult.source !== "unreachable") {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (registryFetchResult.everConfigured !== true) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
throw new NpmInstallBlocked({
|
|
458
|
+
reason: "registry_unreachable",
|
|
459
|
+
packages,
|
|
460
|
+
hasAnyRegistryConfigured: true,
|
|
304
461
|
});
|
|
305
462
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Derive the `hasAnyRegistryConfigured` tri-state from the
|
|
465
|
+
* `NpmRegistryFetchResult` that the immediately-preceding
|
|
466
|
+
* `prepareForPrivateRegistry` call returned.
|
|
467
|
+
*
|
|
468
|
+
* - `true` — `configured` or `stale`. At least one row is known
|
|
469
|
+
* to exist (fresh fetch or last-known-good cache).
|
|
470
|
+
* - `false` — `not-configured`. The kill switch is off OR the
|
|
471
|
+
* server returned an empty list. A deliberate "no rows"
|
|
472
|
+
* observation.
|
|
473
|
+
* - `undefined` — `prepareForPrivateRegistry` returned `undefined` (no
|
|
474
|
+
* client wired in) OR the client resolved to `unreachable`
|
|
475
|
+
* (server down with no usable cache). Both mean "we don't
|
|
476
|
+
* know"; `buildBlockedMessage` falls back to the default
|
|
477
|
+
* variant rather than asserting a row count.
|
|
478
|
+
*
|
|
479
|
+
* Kept narrow + private so the signal stays a single derivation site rather
|
|
480
|
+
* than something every catch handler reimplements.
|
|
481
|
+
*/
|
|
482
|
+
static deriveHasAnyRegistryConfigured(result) {
|
|
483
|
+
if (!result)
|
|
484
|
+
return undefined;
|
|
485
|
+
switch (result.source) {
|
|
486
|
+
case "configured":
|
|
487
|
+
case "stale":
|
|
488
|
+
return true;
|
|
489
|
+
case "not-configured":
|
|
490
|
+
return false;
|
|
491
|
+
case "unreachable":
|
|
492
|
+
return undefined;
|
|
319
493
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Convert an exec failure from `npm --json` into a structured error.
|
|
497
|
+
*
|
|
498
|
+
* - When the stdout JSON matches a known registry-blocked condition (E404,
|
|
499
|
+
* ENOTFOUND, E401, ...) we throw `NpmInstallBlocked` so the tool layer
|
|
500
|
+
* can surface it to the agent as a structured result.
|
|
501
|
+
* - Otherwise we fall back to the legacy `NpmInstallError(stderr)` so any
|
|
502
|
+
* pre-existing handler (e.g. agent-planning's POST_PROCESSING_ERRORED
|
|
503
|
+
* branch) behaves exactly as it did before.
|
|
504
|
+
*
|
|
505
|
+
* Only used on the npm code path; pnpm continues to throw `NpmInstallError`
|
|
506
|
+
* directly because pnpm's `--json` error contract differs and the
|
|
507
|
+
* registry-blocked scenarios only matter in cloud-edit (which forces npm).
|
|
508
|
+
*
|
|
509
|
+
* `registryFetchResult` carries the most recent
|
|
510
|
+
* `prepareForPrivateRegistry` result so the structured error can encode
|
|
511
|
+
* whether the org had any configured registry rows at the moment of
|
|
512
|
+
* failure (used by `buildBlockedMessage` to choose between default and
|
|
513
|
+
* row-aware advice variants). The active llmobs span is annotated with the
|
|
514
|
+
* same data points so a downstream `npm.install_blocked.*` facet can
|
|
515
|
+
* correlate reason, host, and row presence.
|
|
516
|
+
*/
|
|
517
|
+
classifyNpmExecError(error, requestedPackages, registryFetchResult) {
|
|
518
|
+
const stdout = error.stdout ?? "";
|
|
519
|
+
const hasAnyRegistryConfigured = AppShell.deriveHasAnyRegistryConfigured(registryFetchResult);
|
|
520
|
+
const blocked = parseNpmJsonError(stdout, {
|
|
521
|
+
requestedPackages,
|
|
522
|
+
hasAnyRegistryConfigured,
|
|
523
|
+
});
|
|
524
|
+
if (blocked) {
|
|
525
|
+
// Span tags for `superblocks.npm.install_blocked.*` faceting. Stringified
|
|
526
|
+
// because llmobs tag values are strings; `unknown` is used in lieu of
|
|
527
|
+
// `null`/`undefined` so the tag's cardinality stays bounded and a
|
|
528
|
+
// "field absent" datapoint is still queryable.
|
|
529
|
+
//
|
|
530
|
+
// `npm.install_blocked.registry_host` is routed through the P6.1
|
|
531
|
+
// `bucketNpmRegistryHost` sanitizer (same helper used by
|
|
532
|
+
// `recordNpmInstall`) so the tag value is a bounded enum
|
|
533
|
+
// (`public_npm` / `private` / `unknown`). `NpmInstallBlocked` only
|
|
534
|
+
// scrubs credentials and control characters — without the bucketer the
|
|
535
|
+
// raw customer registry hostname would land in llmobs tags, leaking
|
|
536
|
+
// customer-internal hosts and giving the tag unbounded cardinality.
|
|
537
|
+
llmobs.annotate({
|
|
538
|
+
tags: {
|
|
539
|
+
"npm.install_blocked.reason": blocked.reason,
|
|
540
|
+
"npm.install_blocked.registry_host": bucketNpmRegistryHost(blocked.registryHost),
|
|
541
|
+
"npm.install_blocked.npm_error_code": blocked.npmErrorCode ?? "unknown",
|
|
542
|
+
"npm.install_blocked.has_any_registry_configured": hasAnyRegistryConfigured === undefined
|
|
543
|
+
? "unknown"
|
|
544
|
+
: String(hasAnyRegistryConfigured),
|
|
545
|
+
},
|
|
330
546
|
});
|
|
331
|
-
|
|
332
|
-
throw new Error("Could not detect package manager");
|
|
333
|
-
}
|
|
334
|
-
packageManagerAgent = packageManager.agent;
|
|
547
|
+
return blocked;
|
|
335
548
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
549
|
+
// Non-registry-blocked failure (ERESOLVE, EPEERINVALID, ETARGET, …).
|
|
550
|
+
// Prefer the JSON diagnostic from stdout because under `--json` stderr
|
|
551
|
+
// is sparse — preserving the diagnostic keeps agent-planning's
|
|
552
|
+
// POST_PROCESSING_ERRORED debug message useful instead of collapsing
|
|
553
|
+
// it to a generic exec-failure string.
|
|
554
|
+
const diagnostic = parseNpmJsonDiagnostic(stdout);
|
|
555
|
+
if (stdout && !diagnostic) {
|
|
556
|
+
// npm was invoked with `--json` but neither parser could extract
|
|
557
|
+
// anything from stdout. This silently bypasses the row-aware advice
|
|
558
|
+
// matrix, the structured warn log, and the llmobs span tags above —
|
|
559
|
+
// converting the silent fallback into a queryable signal so operators
|
|
560
|
+
// can find these (npm crash mid-output, stdout truncation, schema
|
|
561
|
+
// drift across npm versions, an interactive prompt sneaking past
|
|
562
|
+
// `--json`). Single line, debug-grade payload (truncated preview
|
|
563
|
+
// only) to keep log volume bounded.
|
|
564
|
+
//
|
|
565
|
+
// Scrub BEFORE slicing: raw npm `--json` stdout can contain registry
|
|
566
|
+
// URLs with userinfo or `_authToken=` lines from a leaked `.npmrc`,
|
|
567
|
+
// and slicing first could split a credential across the 200-char
|
|
568
|
+
// boundary so the regex no longer matches.
|
|
569
|
+
getLogger().warn("[npm] failed to extract structured error from `npm --json` stdout; falling back to legacy NpmInstallError", { stdoutPreview: scrubSecrets(stdout).slice(0, 200) });
|
|
339
570
|
}
|
|
340
|
-
|
|
571
|
+
return new NpmInstallError(diagnostic || error.stderr || error.message || "");
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Wraps an install attempt so that a `registry_auth_failed` failure
|
|
575
|
+
* triggers one cache-invalidation + re-fetch + retry cycle before
|
|
576
|
+
* throwing. The helper owns the `.npmrc` materialisation so the retry
|
|
577
|
+
* picks up a freshly-fetched token without duplicating the prep call
|
|
578
|
+
* in every install method.
|
|
579
|
+
*
|
|
580
|
+
* Guard: only one retry per call — a second `registry_auth_failed`
|
|
581
|
+
* propagates unchanged.
|
|
582
|
+
*/
|
|
583
|
+
async withAuthFailedRetry(attempt) {
|
|
584
|
+
const registryFetchResult = await this.enqueueFsOperation("maybeWriteNpmrc", () => prepareForPrivateRegistry(this.appRootDirPath, { preserveScopeLines: PRESERVE_NPMRC_SCOPES }, this.npmRegistryClient));
|
|
341
585
|
try {
|
|
342
|
-
|
|
343
|
-
cwd: this.appRootDirPath,
|
|
344
|
-
signal: abortSignal,
|
|
345
|
-
});
|
|
346
|
-
return stdout;
|
|
586
|
+
return await attempt(registryFetchResult);
|
|
347
587
|
}
|
|
348
588
|
catch (error) {
|
|
349
|
-
|
|
589
|
+
if (error instanceof NpmInstallBlocked &&
|
|
590
|
+
error.reason === "registry_auth_failed" &&
|
|
591
|
+
this.npmRegistryClient) {
|
|
592
|
+
this.npmRegistryClient.invalidateCache();
|
|
593
|
+
getLogger().info("[npm-registry] invalidated cache after registry_auth_failed; retrying install once");
|
|
594
|
+
llmobs.annotate({
|
|
595
|
+
tags: {
|
|
596
|
+
"npm.registry.cache_invalidated_on_auth_failed": "true",
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
const retryFetchResult = await this.enqueueFsOperation("maybeWriteNpmrc", () => prepareForPrivateRegistry(this.appRootDirPath, { preserveScopeLines: PRESERVE_NPMRC_SCOPES }, this.npmRegistryClient));
|
|
600
|
+
return await attempt(retryFetchResult);
|
|
601
|
+
}
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async runPackageManagerInstall(abortSignal) {
|
|
606
|
+
const installStart = Date.now();
|
|
607
|
+
let outcome = "success";
|
|
608
|
+
let capturedFetchResult;
|
|
609
|
+
try {
|
|
610
|
+
return await this.withAuthFailedRetry(async (registryFetchResult) => {
|
|
611
|
+
capturedFetchResult = registryFetchResult;
|
|
612
|
+
// APPS-4370: short-circuit before any exec when server is unreachable
|
|
613
|
+
// AND this org's marker says it was previously configured.
|
|
614
|
+
this.maybeShortCircuitForUnreachable(registryFetchResult, []);
|
|
615
|
+
const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
|
|
616
|
+
// In cloud edit mode, always use npm because pnpm is not available in the container.
|
|
617
|
+
// The user's project may have pnpm signals (pnpm-lock.yaml, packageManager field)
|
|
618
|
+
// from local editing, but we must use npm in cloud mode.
|
|
619
|
+
const isCloudMode = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
|
|
620
|
+
let packageManagerAgent;
|
|
621
|
+
if (isCloudMode) {
|
|
622
|
+
// Force npm in cloud mode
|
|
623
|
+
packageManagerAgent = "npm";
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
// NOTE: this largely duplicates the logic in the CLI
|
|
627
|
+
const packageManager = await detect({
|
|
628
|
+
strategies: [
|
|
629
|
+
"packageManager-field",
|
|
630
|
+
"lockfile",
|
|
631
|
+
"install-metadata",
|
|
632
|
+
"devEngines-field",
|
|
633
|
+
],
|
|
634
|
+
cwd: this.appRootDirPath,
|
|
635
|
+
});
|
|
636
|
+
if (!packageManager) {
|
|
637
|
+
throw new Error("Could not detect package manager");
|
|
638
|
+
}
|
|
639
|
+
packageManagerAgent = packageManager.agent;
|
|
640
|
+
}
|
|
641
|
+
const isNpm = packageManagerAgent === "npm";
|
|
642
|
+
const baseInstallArgs = isNpm
|
|
643
|
+
? ["--fund=false", "--audit=false", "--json"]
|
|
644
|
+
: [];
|
|
645
|
+
const installArgs = ignoreScripts
|
|
646
|
+
? [...baseInstallArgs, "--ignore-scripts"]
|
|
647
|
+
: baseInstallArgs;
|
|
648
|
+
const installCommand = resolveCommand(packageManagerAgent, "install", installArgs);
|
|
649
|
+
if (!installCommand) {
|
|
650
|
+
throw new Error(`Could not determine install command for ${packageManagerAgent}, skipping package installation`);
|
|
651
|
+
}
|
|
652
|
+
const { command, args } = installCommand;
|
|
653
|
+
try {
|
|
654
|
+
const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
|
|
655
|
+
cwd: this.appRootDirPath,
|
|
656
|
+
env: this.installLogEnv(),
|
|
657
|
+
signal: abortSignal,
|
|
658
|
+
// See `NPM_JSON_MAX_BUFFER` — `--json` summary can outgrow the
|
|
659
|
+
// default 1 MiB exec buffer on fat dep trees.
|
|
660
|
+
maxBuffer: NPM_JSON_MAX_BUFFER,
|
|
661
|
+
});
|
|
662
|
+
// On the npm path stdout is a JSON blob; translate to a compact
|
|
663
|
+
// human-readable summary for the agent, falling back to raw stdout
|
|
664
|
+
// when the JSON shape drifts.
|
|
665
|
+
if (isNpm) {
|
|
666
|
+
return parseNpmJsonSuccess(stdout) ?? stdout;
|
|
667
|
+
}
|
|
668
|
+
return stdout;
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
if (isNpm)
|
|
672
|
+
throw this.classifyNpmExecError(error, undefined, registryFetchResult);
|
|
673
|
+
throw new NpmInstallError(error.stderr || error.message);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
outcome = outcomeFromInstallError(err);
|
|
679
|
+
throw err;
|
|
680
|
+
}
|
|
681
|
+
finally {
|
|
682
|
+
this.observeNpmInstall("install_all", capturedFetchResult, outcome, Date.now() - installStart);
|
|
350
683
|
}
|
|
351
684
|
}
|
|
352
685
|
async installSpecificPackages(packages, abortSignal) {
|
|
353
686
|
if (!packages || packages.length === 0) {
|
|
354
687
|
throw new Error("No packages specified for installation");
|
|
355
688
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
"
|
|
376
|
-
|
|
377
|
-
|
|
689
|
+
const installStart = Date.now();
|
|
690
|
+
let outcome = "success";
|
|
691
|
+
let capturedFetchResult;
|
|
692
|
+
try {
|
|
693
|
+
return await this.withAuthFailedRetry(async (registryFetchResult) => {
|
|
694
|
+
capturedFetchResult = registryFetchResult;
|
|
695
|
+
// APPS-4370: short-circuit before exec when unreachable + everConfigured.
|
|
696
|
+
// Pass through the requested package specs so the structured error
|
|
697
|
+
// surfaces them to the agent ("we tried to install lodash and the
|
|
698
|
+
// private registry was unreachable").
|
|
699
|
+
this.maybeShortCircuitForUnreachable(registryFetchResult, packages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name));
|
|
700
|
+
const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
|
|
701
|
+
// In cloud edit mode, always use npm because pnpm is not available in the container.
|
|
702
|
+
// The user's project may have pnpm signals (pnpm-lock.yaml, packageManager field)
|
|
703
|
+
// from local editing, but we must use npm in cloud mode.
|
|
704
|
+
const isCloudMode = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
|
|
705
|
+
let packageManagerAgent;
|
|
706
|
+
if (isCloudMode) {
|
|
707
|
+
// Force npm in cloud mode
|
|
708
|
+
packageManagerAgent = "npm";
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
const packageManager = await detect({
|
|
712
|
+
strategies: [
|
|
713
|
+
"packageManager-field",
|
|
714
|
+
"lockfile",
|
|
715
|
+
"install-metadata",
|
|
716
|
+
"devEngines-field",
|
|
717
|
+
],
|
|
718
|
+
cwd: this.appRootDirPath,
|
|
719
|
+
});
|
|
720
|
+
if (!packageManager) {
|
|
721
|
+
throw new Error("Could not detect package manager");
|
|
722
|
+
}
|
|
723
|
+
packageManagerAgent = packageManager.agent;
|
|
724
|
+
}
|
|
725
|
+
const isNpm = packageManagerAgent === "npm";
|
|
726
|
+
// Separate regular and dev dependencies
|
|
727
|
+
const regularPackages = packages.filter((pkg) => !pkg.dev);
|
|
728
|
+
const devPackages = packages.filter((pkg) => pkg.dev);
|
|
729
|
+
const outputs = [];
|
|
730
|
+
// Install regular dependencies
|
|
731
|
+
if (regularPackages.length > 0) {
|
|
732
|
+
const packageSpecs = regularPackages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name);
|
|
733
|
+
const baseArgs = isNpm
|
|
734
|
+
? ["--fund=false", "--audit=false", "--json", ...packageSpecs]
|
|
735
|
+
: packageSpecs;
|
|
736
|
+
const addCommand = resolveCommand(packageManagerAgent, "add", ignoreScripts ? ["--ignore-scripts", ...baseArgs] : baseArgs);
|
|
737
|
+
if (!addCommand) {
|
|
738
|
+
throw new Error(`Could not determine add command for ${packageManagerAgent}`);
|
|
739
|
+
}
|
|
740
|
+
const { command, args } = addCommand;
|
|
741
|
+
try {
|
|
742
|
+
const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
|
|
743
|
+
cwd: this.appRootDirPath,
|
|
744
|
+
env: this.installLogEnv(),
|
|
745
|
+
signal: abortSignal,
|
|
746
|
+
// See `NPM_JSON_MAX_BUFFER` — `--json` summary can outgrow the
|
|
747
|
+
// default 1 MiB exec buffer on fat dep trees.
|
|
748
|
+
maxBuffer: NPM_JSON_MAX_BUFFER,
|
|
749
|
+
});
|
|
750
|
+
outputs.push(isNpm ? (parseNpmJsonSuccess(stdout) ?? stdout) : stdout);
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
if (isNpm)
|
|
754
|
+
throw this.classifyNpmExecError(error, regularPackages, registryFetchResult);
|
|
755
|
+
throw new NpmInstallError(error.stderr || error.message);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// Install dev dependencies
|
|
759
|
+
if (devPackages.length > 0) {
|
|
760
|
+
const packageSpecs = devPackages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name);
|
|
761
|
+
const baseArgs = isNpm
|
|
762
|
+
? [
|
|
763
|
+
"--save-dev",
|
|
764
|
+
"--fund=false",
|
|
765
|
+
"--audit=false",
|
|
766
|
+
"--json",
|
|
767
|
+
...packageSpecs,
|
|
768
|
+
]
|
|
769
|
+
: ["-D", ...packageSpecs];
|
|
770
|
+
const addCommand = resolveCommand(packageManagerAgent, "add", ignoreScripts ? ["--ignore-scripts", ...baseArgs] : baseArgs);
|
|
771
|
+
if (!addCommand) {
|
|
772
|
+
throw new Error(`Could not determine add command for ${packageManagerAgent}`);
|
|
773
|
+
}
|
|
774
|
+
const { command, args } = addCommand;
|
|
775
|
+
try {
|
|
776
|
+
const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
|
|
777
|
+
cwd: this.appRootDirPath,
|
|
778
|
+
env: this.installLogEnv(),
|
|
779
|
+
signal: abortSignal,
|
|
780
|
+
// See `NPM_JSON_MAX_BUFFER` — `--json` summary can outgrow the
|
|
781
|
+
// default 1 MiB exec buffer on fat dep trees.
|
|
782
|
+
maxBuffer: NPM_JSON_MAX_BUFFER,
|
|
783
|
+
});
|
|
784
|
+
outputs.push(isNpm ? (parseNpmJsonSuccess(stdout) ?? stdout) : stdout);
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
if (isNpm)
|
|
788
|
+
throw this.classifyNpmExecError(error, devPackages, registryFetchResult);
|
|
789
|
+
throw new NpmInstallError(error.stderr || error.message);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return outputs.join("\n");
|
|
378
793
|
});
|
|
379
|
-
if (!packageManager) {
|
|
380
|
-
throw new Error("Could not detect package manager");
|
|
381
|
-
}
|
|
382
|
-
packageManagerAgent = packageManager.agent;
|
|
383
|
-
}
|
|
384
|
-
// Separate regular and dev dependencies
|
|
385
|
-
const regularPackages = packages.filter((pkg) => !pkg.dev);
|
|
386
|
-
const devPackages = packages.filter((pkg) => pkg.dev);
|
|
387
|
-
const outputs = [];
|
|
388
|
-
// Install regular dependencies
|
|
389
|
-
if (regularPackages.length > 0) {
|
|
390
|
-
const packageSpecs = regularPackages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name);
|
|
391
|
-
const addCommand = resolveCommand(packageManagerAgent, "add", packageManagerAgent === "npm"
|
|
392
|
-
? ["--fund=false", "--audit=false", ...packageSpecs]
|
|
393
|
-
: packageSpecs);
|
|
394
|
-
if (!addCommand) {
|
|
395
|
-
throw new Error(`Could not determine add command for ${packageManagerAgent}`);
|
|
396
|
-
}
|
|
397
|
-
const { command, args } = addCommand;
|
|
398
|
-
try {
|
|
399
|
-
const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
|
|
400
|
-
cwd: this.appRootDirPath,
|
|
401
|
-
signal: abortSignal,
|
|
402
|
-
});
|
|
403
|
-
outputs.push(stdout);
|
|
404
|
-
}
|
|
405
|
-
catch (error) {
|
|
406
|
-
throw new NpmInstallError(error.stderr || error.message);
|
|
407
|
-
}
|
|
408
794
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (!addCommand) {
|
|
416
|
-
throw new Error(`Could not determine add command for ${packageManagerAgent}`);
|
|
417
|
-
}
|
|
418
|
-
const { command, args } = addCommand;
|
|
419
|
-
try {
|
|
420
|
-
const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
|
|
421
|
-
cwd: this.appRootDirPath,
|
|
422
|
-
signal: abortSignal,
|
|
423
|
-
});
|
|
424
|
-
outputs.push(stdout);
|
|
425
|
-
}
|
|
426
|
-
catch (error) {
|
|
427
|
-
throw new NpmInstallError(error.stderr || error.message);
|
|
428
|
-
}
|
|
795
|
+
catch (err) {
|
|
796
|
+
outcome = outcomeFromInstallError(err);
|
|
797
|
+
throw err;
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
this.observeNpmInstall("install_specific", capturedFetchResult, outcome, Date.now() - installStart);
|
|
429
801
|
}
|
|
430
|
-
return outputs.join("\n");
|
|
431
802
|
}
|
|
432
803
|
async uninstallPackages(packages, options) {
|
|
433
|
-
// Uninstalls hit the same registry resolution rules as installs (
|
|
434
|
-
// dependency
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
804
|
+
// Uninstalls hit the same registry resolution rules as installs (the
|
|
805
|
+
// dependency-tree solver re-reifies the remaining tree and can re-fetch
|
|
806
|
+
// metadata), so they run through the SAME safety wrapper as every
|
|
807
|
+
// install path rather than a bespoke one-shot exec:
|
|
808
|
+
//
|
|
809
|
+
// - `withAuthFailedRetry` owns the `.npmrc` materialization (via
|
|
810
|
+
// `prepareForPrivateRegistry`, same serialization as `runNpmInstall`)
|
|
811
|
+
// AND retries once on `registry_auth_failed` after invalidating the
|
|
812
|
+
// registry cache, so an uninstall against a deployment whose
|
|
813
|
+
// private-registry token was rotated mid-session recovers instead of
|
|
814
|
+
// hard-failing on the stale `_authToken=` line.
|
|
815
|
+
// - `maybeShortCircuitForUnreachable` (APPS-4370) refuses to spawn when
|
|
816
|
+
// the config server is unreachable AND this org was previously
|
|
817
|
+
// configured. Without it the uninstall escaped the fail-closed gate,
|
|
818
|
+
// 401'd against the stale token, and surfaced `registry_auth_failed`
|
|
819
|
+
// (rotate-credentials advice) instead of `registry_unreachable`
|
|
820
|
+
// (registry-down advice) — the wrong remediation.
|
|
821
|
+
// - The npm path runs with `--json` and routes failures through
|
|
822
|
+
// `classifyNpmExecError`, identical to `installSpecificPackages`, so
|
|
823
|
+
// E404 / E401 / ENOTFOUND surface as a structured `NpmInstallBlocked`
|
|
824
|
+
// (and the auth-failed case feeds the retry above).
|
|
825
|
+
//
|
|
826
|
+
// `shouldIgnoreInstallScripts` honours the org's `allowInstallScripts`
|
|
827
|
+
// policy on uninstall too — `preuninstall` / `postuninstall` lifecycle
|
|
828
|
+
// scripts are just as dangerous as install-time ones.
|
|
829
|
+
return await this.withAuthFailedRetry(async (registryFetchResult) => {
|
|
830
|
+
this.maybeShortCircuitForUnreachable(registryFetchResult, packages);
|
|
831
|
+
const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
|
|
832
|
+
// pnpm in local dev (avoids store/lockfile conflicts with the
|
|
833
|
+
// surrounding workspace); npm everywhere else so the `--json` error
|
|
834
|
+
// contract — and therefore the registry-blocked advice matrix —
|
|
835
|
+
// applies. Mirrors `runNpmInstall` / `installSpecificPackages`.
|
|
836
|
+
const isNpm = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
|
|
837
|
+
const packageManager = isNpm ? "npm" : "pnpm";
|
|
838
|
+
const uninstallCmd = isNpm ? "uninstall" : "remove";
|
|
839
|
+
const jsonFlag = isNpm ? " --json" : "";
|
|
840
|
+
const baseCommand = `${packageManager} ${uninstallCmd} ${packages.join(" ")}${jsonFlag}`;
|
|
841
|
+
const command = ignoreScripts
|
|
842
|
+
? `${baseCommand} --ignore-scripts`
|
|
843
|
+
: baseCommand;
|
|
844
|
+
return await new Promise((resolve, reject) => {
|
|
845
|
+
exec(command, {
|
|
846
|
+
cwd: this.appRootDirPath,
|
|
847
|
+
env: this.installLogEnv(),
|
|
848
|
+
timeout: 30_000,
|
|
849
|
+
signal: options?.abortSignal,
|
|
850
|
+
// `--json` summary can outgrow the default 1 MiB exec buffer.
|
|
851
|
+
maxBuffer: NPM_JSON_MAX_BUFFER,
|
|
852
|
+
}, (error, stdout, stderr) => {
|
|
853
|
+
if (error) {
|
|
854
|
+
// Same classifier as the install paths: a registry block
|
|
855
|
+
// (E404 / E401 / ENOTFOUND, ...) becomes a structured
|
|
856
|
+
// `NpmInstallBlocked` (with row-aware advice + the
|
|
857
|
+
// auth-failed retry signal consumed by `withAuthFailedRetry`);
|
|
858
|
+
// anything else falls back to the legacy `NpmInstallError`.
|
|
859
|
+
// pnpm doesn't emit the `--json` envelope, so it keeps the
|
|
860
|
+
// raw-stderr error.
|
|
861
|
+
if (isNpm) {
|
|
862
|
+
return reject(this.classifyNpmExecError({ stdout, stderr, message: error.message }, undefined, registryFetchResult));
|
|
863
|
+
}
|
|
864
|
+
return reject(new NpmInstallError(stderr || error.message));
|
|
865
|
+
}
|
|
866
|
+
// On the npm path stdout is a `--json` blob; collapse it to a
|
|
867
|
+
// compact human-readable summary (same as
|
|
868
|
+
// `installSpecificPackages`), falling back to raw stdout on
|
|
869
|
+
// shape drift.
|
|
870
|
+
return resolve({
|
|
871
|
+
stdout: isNpm
|
|
872
|
+
? (parseNpmJsonSuccess(stdout) ?? stdout)
|
|
873
|
+
: stdout,
|
|
874
|
+
stderr,
|
|
875
|
+
});
|
|
876
|
+
});
|
|
455
877
|
});
|
|
456
878
|
});
|
|
457
879
|
}
|