@unbrained/pm-cli 2026.5.24 → 2026.5.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +946 -525
- package/README.md +2 -10
- package/dist/cli/bootstrap-args.d.ts +18 -1
- package/dist/cli/bootstrap-args.js +143 -3
- package/dist/cli/bootstrap-args.js.map +1 -1
- package/dist/cli/commander-usage.js +134 -11
- package/dist/cli/commander-usage.js.map +1 -1
- package/dist/cli/commands/append.js +4 -3
- package/dist/cli/commands/append.js.map +1 -1
- package/dist/cli/commands/claim.js +5 -4
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/close.js +4 -3
- package/dist/cli/commands/close.js.map +1 -1
- package/dist/cli/commands/completion.d.ts +2 -2
- package/dist/cli/commands/completion.js +109 -56
- package/dist/cli/commands/completion.js.map +1 -1
- package/dist/cli/commands/config.d.ts +1 -1
- package/dist/cli/commands/config.js +82 -4
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/create.js +7 -272
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/commands/delete.js +4 -3
- package/dist/cli/commands/delete.js.map +1 -1
- package/dist/cli/commands/docs.d.ts +1 -12
- package/dist/cli/commands/docs.js +8 -312
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/extension/bundled-catalog.d.ts +14 -0
- package/dist/cli/commands/extension/bundled-catalog.js +268 -0
- package/dist/cli/commands/extension/bundled-catalog.js.map +1 -0
- package/dist/cli/commands/extension/doctor.d.ts +31 -0
- package/dist/cli/commands/extension/doctor.js +345 -0
- package/dist/cli/commands/extension/doctor.js.map +1 -0
- package/dist/cli/commands/extension/install-sources.d.ts +37 -0
- package/dist/cli/commands/extension/install-sources.js +384 -0
- package/dist/cli/commands/extension/install-sources.js.map +1 -0
- package/dist/cli/commands/extension/managed-state.d.ts +48 -0
- package/dist/cli/commands/extension/managed-state.js +172 -0
- package/dist/cli/commands/extension/managed-state.js.map +1 -0
- package/dist/cli/commands/extension/scaffold.d.ts +14 -0
- package/dist/cli/commands/extension/scaffold.js +169 -0
- package/dist/cli/commands/extension/scaffold.js.map +1 -0
- package/dist/cli/commands/extension/shared.d.ts +14 -0
- package/dist/cli/commands/extension/shared.js +106 -0
- package/dist/cli/commands/extension/shared.js.map +1 -0
- package/dist/cli/commands/extension.d.ts +36 -68
- package/dist/cli/commands/extension.js +143 -1422
- package/dist/cli/commands/extension.js.map +1 -1
- package/dist/cli/commands/files.d.ts +1 -12
- package/dist/cli/commands/files.js +11 -308
- package/dist/cli/commands/files.js.map +1 -1
- package/dist/cli/commands/get.js +4 -3
- package/dist/cli/commands/get.js.map +1 -1
- package/dist/cli/commands/health.js +17 -3
- package/dist/cli/commands/health.js.map +1 -1
- package/dist/cli/commands/history-redact.js +23 -18
- package/dist/cli/commands/history-redact.js.map +1 -1
- package/dist/cli/commands/history-repair.js +24 -18
- package/dist/cli/commands/history-repair.js.map +1 -1
- package/dist/cli/commands/legacy-none-tokens.d.ts +3 -0
- package/dist/cli/commands/legacy-none-tokens.js +39 -0
- package/dist/cli/commands/legacy-none-tokens.js.map +1 -0
- package/dist/cli/commands/linked-artifacts.d.ts +96 -0
- package/dist/cli/commands/linked-artifacts.js +335 -0
- package/dist/cli/commands/linked-artifacts.js.map +1 -0
- package/dist/cli/commands/linked-test-parsers.d.ts +28 -0
- package/dist/cli/commands/linked-test-parsers.js +192 -0
- package/dist/cli/commands/linked-test-parsers.js.map +1 -0
- package/dist/cli/commands/list.js +19 -4
- package/dist/cli/commands/list.js.map +1 -1
- package/dist/cli/commands/normalize.js +4 -3
- package/dist/cli/commands/normalize.js.map +1 -1
- package/dist/cli/commands/recurrence-parsers.d.ts +26 -0
- package/dist/cli/commands/recurrence-parsers.js +98 -0
- package/dist/cli/commands/recurrence-parsers.js.map +1 -0
- package/dist/cli/commands/restore.js +19 -8
- package/dist/cli/commands/restore.js.map +1 -1
- package/dist/cli/commands/search.js +5 -4
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/test/linked-command-detection.d.ts +37 -0
- package/dist/cli/commands/test/linked-command-detection.js +200 -0
- package/dist/cli/commands/test/linked-command-detection.js.map +1 -0
- package/dist/cli/commands/test.d.ts +1 -2
- package/dist/cli/commands/test.js +7 -349
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/commands/update-many.js +4 -3
- package/dist/cli/commands/update-many.js.map +1 -1
- package/dist/cli/commands/update.js +62 -354
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/error-guidance.d.ts +1 -0
- package/dist/cli/error-guidance.js +6 -2
- package/dist/cli/error-guidance.js.map +1 -1
- package/dist/cli/main.d.ts +11 -0
- package/dist/cli/main.js +76 -28
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/register-list-query.d.ts +4 -1
- package/dist/cli/register-list-query.js +242 -203
- package/dist/cli/register-list-query.js.map +1 -1
- package/dist/cli/register-mutation.js +24 -9
- package/dist/cli/register-mutation.js.map +1 -1
- package/dist/cli/register-operations.js +3 -3
- package/dist/cli/register-operations.js.map +1 -1
- package/dist/cli/register-setup.js +12 -7
- package/dist/cli/register-setup.js.map +1 -1
- package/dist/cli/registration-helpers.js +3 -2
- package/dist/cli/registration-helpers.js.map +1 -1
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/config/positional-value.d.ts +44 -0
- package/dist/core/config/positional-value.js +109 -0
- package/dist/core/config/positional-value.js.map +1 -0
- package/dist/core/extensions/extension-capability-aliases.d.ts +14 -0
- package/dist/core/extensions/extension-capability-aliases.js +159 -0
- package/dist/core/extensions/extension-capability-aliases.js.map +1 -0
- package/dist/core/extensions/extension-hook-runtime.d.ts +13 -0
- package/dist/core/extensions/extension-hook-runtime.js +414 -0
- package/dist/core/extensions/extension-hook-runtime.js.map +1 -0
- package/dist/core/extensions/extension-policy.d.ts +69 -0
- package/dist/core/extensions/extension-policy.js +481 -0
- package/dist/core/extensions/extension-policy.js.map +1 -0
- package/dist/core/extensions/extension-registries.d.ts +8 -0
- package/dist/core/extensions/extension-registries.js +52 -0
- package/dist/core/extensions/extension-registries.js.map +1 -0
- package/dist/core/extensions/extension-runtime-helpers.d.ts +6 -0
- package/dist/core/extensions/extension-runtime-helpers.js +29 -0
- package/dist/core/extensions/extension-runtime-helpers.js.map +1 -0
- package/dist/core/extensions/extension-types.d.ts +13 -39
- package/dist/core/extensions/extension-types.js +34 -2
- package/dist/core/extensions/extension-types.js.map +1 -1
- package/dist/core/extensions/index.d.ts +7 -0
- package/dist/core/extensions/index.js +11 -2
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +4 -22
- package/dist/core/extensions/loader.js +22 -1139
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/history/drift-scan.d.ts +11 -0
- package/dist/core/history/drift-scan.js +114 -32
- package/dist/core/history/drift-scan.js.map +1 -1
- package/dist/core/history/history-rewrite.d.ts +43 -0
- package/dist/core/history/history-rewrite.js +48 -0
- package/dist/core/history/history-rewrite.js.map +1 -0
- package/dist/core/history/history.js +5 -4
- package/dist/core/history/history.js.map +1 -1
- package/dist/core/history/replay.js +4 -3
- package/dist/core/history/replay.js.map +1 -1
- package/dist/core/item/item-record.d.ts +19 -0
- package/dist/core/item/item-record.js +24 -0
- package/dist/core/item/item-record.js.map +1 -0
- package/dist/core/output/mutation-projection.d.ts +31 -0
- package/dist/core/output/mutation-projection.js +103 -0
- package/dist/core/output/mutation-projection.js.map +1 -0
- package/dist/core/output/output.d.ts +2 -0
- package/dist/core/output/output.js +5 -3
- package/dist/core/output/output.js.map +1 -1
- package/dist/core/schema/runtime-schema.js +8 -38
- package/dist/core/schema/runtime-schema.js.map +1 -1
- package/dist/core/search/vector-stores.js +46 -9
- package/dist/core/search/vector-stores.js.map +1 -1
- package/dist/core/sentry/helpers.d.ts +1 -1
- package/dist/core/sentry/helpers.js +20 -3
- package/dist/core/sentry/helpers.js.map +1 -1
- package/dist/core/shared/command-types.d.ts +1 -0
- package/dist/core/shared/command-types.js +2 -2
- package/dist/core/shared/command-types.js.map +1 -1
- package/dist/core/shared/constants.d.ts +10 -1
- package/dist/core/shared/constants.js +56 -58
- package/dist/core/shared/constants.js.map +1 -1
- package/dist/core/shared/primitives.d.ts +23 -0
- package/dist/core/shared/primitives.js +39 -2
- package/dist/core/shared/primitives.js.map +1 -1
- package/dist/core/store/front-matter-cache.d.ts +16 -2
- package/dist/core/store/front-matter-cache.js +99 -33
- package/dist/core/store/front-matter-cache.js.map +1 -1
- package/dist/core/store/item-store.js +8 -73
- package/dist/core/store/item-store.js.map +1 -1
- package/dist/mcp/server.js +76 -28
- package/dist/mcp/server.js.map +1 -1
- package/dist/sdk/cli-contracts/enum-contracts.d.ts +20 -0
- package/dist/sdk/cli-contracts/enum-contracts.js +156 -0
- package/dist/sdk/cli-contracts/enum-contracts.js.map +1 -0
- package/dist/sdk/cli-contracts/tool-option-contracts.d.ts +14 -0
- package/dist/sdk/cli-contracts/tool-option-contracts.js +243 -0
- package/dist/sdk/cli-contracts/tool-option-contracts.js.map +1 -0
- package/dist/sdk/cli-contracts/tool-parameter-tables.d.ts +11 -0
- package/dist/sdk/cli-contracts/tool-parameter-tables.js +901 -0
- package/dist/sdk/cli-contracts/tool-parameter-tables.js.map +1 -0
- package/dist/sdk/cli-contracts.d.ts +11 -33
- package/dist/sdk/cli-contracts.js +23 -1356
- package/dist/sdk/cli-contracts.js.map +1 -1
- package/dist/sdk/package-import-adapters.d.ts +74 -0
- package/dist/sdk/package-import-adapters.js +186 -0
- package/dist/sdk/package-import-adapters.js.map +1 -0
- package/dist/sdk/package-runtime-options.d.ts +26 -0
- package/dist/sdk/package-runtime-options.js +71 -0
- package/dist/sdk/package-runtime-options.js.map +1 -0
- package/dist/sdk/runtime.d.ts +2 -0
- package/dist/sdk/runtime.js +4 -2
- package/dist/sdk/runtime.js.map +1 -1
- package/docs/AGENT_GUIDE.md +6 -10
- package/docs/CLAUDE_CODE_PLUGIN.md +5 -28
- package/docs/CODEX_PLUGIN.md +5 -5
- package/docs/COMMANDS.md +19 -3
- package/docs/CONFIGURATION.md +15 -0
- package/docs/EXTENSIONS.md +4 -63
- package/docs/RELEASING.md +4 -4
- package/marketplace.json +7 -3
- package/package.json +9 -6
- package/packages/pm-beads/extensions/beads/index.js +2 -49
- package/packages/pm-beads/extensions/beads/index.ts +2 -54
- package/packages/pm-beads/extensions/beads/runtime-loader.js +86 -0
- package/packages/pm-beads/extensions/beads/runtime-loader.ts +88 -0
- package/packages/pm-beads/extensions/beads/runtime.js +26 -115
- package/packages/pm-beads/extensions/beads/runtime.ts +33 -132
- package/packages/pm-calendar/extensions/calendar/index.js +47 -2
- package/packages/pm-calendar/extensions/calendar/index.ts +52 -2
- package/packages/pm-calendar/extensions/calendar/runtime.js +1 -0
- package/packages/pm-calendar/extensions/calendar/runtime.ts +1 -0
- package/packages/pm-governance-audit/extensions/governance-audit/runtime.js +14 -41
- package/packages/pm-governance-audit/extensions/governance-audit/runtime.ts +25 -41
- package/packages/pm-guide-shell/extensions/guide-shell/runtime.js +10 -50
- package/packages/pm-guide-shell/extensions/guide-shell/runtime.ts +17 -50
- package/packages/pm-linked-test-adapters/extensions/linked-test-adapters/runtime.js +8 -40
- package/packages/pm-linked-test-adapters/extensions/linked-test-adapters/runtime.ts +10 -40
- package/packages/pm-search-advanced/extensions/search-advanced/index.js +1 -1
- package/packages/pm-search-advanced/extensions/search-advanced/runtime.js +4 -37
- package/packages/pm-search-advanced/extensions/search-advanced/runtime.ts +6 -37
- package/packages/pm-todos/extensions/todos/index.js +3 -50
- package/packages/pm-todos/extensions/todos/index.ts +3 -55
- package/packages/pm-todos/extensions/todos/runtime-loader.js +86 -0
- package/packages/pm-todos/extensions/todos/runtime-loader.ts +88 -0
- package/packages/pm-todos/extensions/todos/runtime.js +24 -117
- package/packages/pm-todos/extensions/todos/runtime.ts +32 -129
- package/plugins/pm-claude/README.md +2 -2
- package/plugins/pm-claude/commands/pm-planner.md +1 -15
- package/plugins/pm-claude/scripts/pm-mcp-server.mjs +5 -2
- package/plugins/pm-claude/skills/pm-planner/SKILL.md +3 -21
- package/plugins/pm-codex/scripts/pm-mcp-server.mjs +15 -6
- package/plugins/pm-codex/skills/pm-native/SKILL.md +1 -13
- package/PRD.md +0 -1734
|
@@ -1,156 +1,36 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="bec21924-aaa9-50f0-83ba-af92671813ea")}catch(e){}}();
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
|
-
import os from "node:os";
|
|
6
5
|
import path from "node:path";
|
|
7
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
6
|
import { promisify } from "node:util";
|
|
9
7
|
import { activateExtensions, loadExtensions, nextExtensionReloadToken } from "../../core/extensions/index.js";
|
|
10
|
-
import {
|
|
8
|
+
import { resolveExtensionRoots } from "../../core/extensions/loader.js";
|
|
11
9
|
import { pathExists } from "../../core/fs/fs-utils.js";
|
|
12
|
-
import { isPathWithinDirectory } from "../../core/fs/path-utils.js";
|
|
13
|
-
import { PM_PACKAGE_RESOURCE_KINDS, collectPackageExtensionDirectories, readPmPackageManifest, } from "../../core/packages/manifest.js";
|
|
14
|
-
import { resolvePmPackageRootFromModule } from "../../core/packages/root.js";
|
|
15
10
|
import { EXIT_CODE } from "../../core/shared/constants.js";
|
|
16
11
|
import { PmCliError } from "../../core/shared/errors.js";
|
|
17
12
|
import { nowIso } from "../../core/shared/time.js";
|
|
18
13
|
import { resolveGlobalPmRoot, resolvePmRoot } from "../../core/store/paths.js";
|
|
19
14
|
import { readSettings, writeSettings } from "../../core/store/settings.js";
|
|
15
|
+
// Cohesive helper groups now live in ./extension/* sibling modules. They are
|
|
16
|
+
// imported for the command wiring that stays here and re-exported below so
|
|
17
|
+
// existing import sites (sdk barrels, upgrade.ts, tests) keep importing the
|
|
18
|
+
// public surface (runExtension, managed-state read/write, install-source
|
|
19
|
+
// parsing, …) from "./extension.js" unchanged.
|
|
20
|
+
import { normalizeStringList, normalizeExtensionNameForMatch, normalizeManagedDirectoryName, parseExtensionManifest, validateExtensionDirectory, } from "./extension/shared.js";
|
|
21
|
+
import { sortManagedEntries, managedExtensionSourcesEquivalent, readManagedExtensionState, writeManagedExtensionState, upsertManagedEntry, resolveManagedExtensionStatePath, } from "./extension/managed-state.js";
|
|
22
|
+
import { parseExtensionInstallSource, resolveInstallSource, areDirectoriesEquivalent, runGitCommand, } from "./extension/install-sources.js";
|
|
23
|
+
import { resolveBundledExtensionAliasSource, isBundledPackageInstallAllTarget, listBundledPackageAliases, resolveBundledAliasManifestName, buildBundledPackageCatalog, } from "./extension/bundled-catalog.js";
|
|
24
|
+
import { scaffoldExtensionProject } from "./extension/scaffold.js";
|
|
25
|
+
import { applyDoctorRuntimeActivationState, classifyDoctorLoadFailureWarnings, buildExtensionTriageSummary, parseDoctorDetailMode, collectUnknownCapabilityGuidance, buildCapabilityContractMetadata, buildDoctorConsistencySummary, } from "./extension/doctor.js";
|
|
26
|
+
// Re-export the public surface that lives in sibling modules but was previously
|
|
27
|
+
// exported from this file (used by sdk barrels, upgrade.ts, and tests).
|
|
28
|
+
export { parseExtensionManifest, validateExtensionDirectory, readManagedExtensionState, writeManagedExtensionState, resolveManagedExtensionStatePath, parseExtensionInstallSource, };
|
|
20
29
|
const execFileAsync = promisify(execFile);
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const LEGACY_BUNDLED_PACKAGE_ALIASES = {
|
|
26
|
-
beads: {
|
|
27
|
-
package_directory: "pm-beads",
|
|
28
|
-
legacy_extension_directory: "beads",
|
|
29
|
-
},
|
|
30
|
-
todos: {
|
|
31
|
-
package_directory: "pm-todos",
|
|
32
|
-
legacy_extension_directory: "todos",
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
const BUNDLED_PACKAGE_INSTALL_ALL_TARGETS = new Set(["*", "all"]);
|
|
36
|
-
function resolvePackageRootCandidates() {
|
|
37
|
-
const candidates = [];
|
|
38
|
-
const envRoot = process.env[PM_PACKAGE_ROOT_ENV];
|
|
39
|
-
if (typeof envRoot === "string" && envRoot.trim().length > 0) {
|
|
40
|
-
candidates.push(path.resolve(envRoot.trim()));
|
|
41
|
-
}
|
|
42
|
-
candidates.push(resolvePmPackageRootFromModule(import.meta.url, ["../../.."]));
|
|
43
|
-
return [...new Set(candidates)];
|
|
44
|
-
}
|
|
45
|
-
async function resolveBundledExtensionAliasSource(input) {
|
|
46
|
-
const normalized = input.trim().toLowerCase();
|
|
47
|
-
const packageRoot = await resolveBundledPackageRoot(normalized);
|
|
48
|
-
if (packageRoot) {
|
|
49
|
-
return packageRoot;
|
|
50
|
-
}
|
|
51
|
-
const alias = LEGACY_BUNDLED_PACKAGE_ALIASES[normalized];
|
|
52
|
-
if (!alias) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
for (const packageRoot of resolvePackageRootCandidates()) {
|
|
56
|
-
const legacyExtensionPath = path.join(packageRoot, ".agents", "pm", "extensions", alias.legacy_extension_directory);
|
|
57
|
-
if (await pathExists(path.join(legacyExtensionPath, "manifest.json"))) {
|
|
58
|
-
return legacyExtensionPath;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
function isBundledPackageInstallAllTarget(input) {
|
|
64
|
-
return BUNDLED_PACKAGE_INSTALL_ALL_TARGETS.has(input.trim().toLowerCase());
|
|
65
|
-
}
|
|
66
|
-
function derivePackageAlias(packageDirectory) {
|
|
67
|
-
return packageDirectory.replace(/^pm-/i, "").trim().toLowerCase();
|
|
68
|
-
}
|
|
69
|
-
async function collectBundledPackageEntries() {
|
|
70
|
-
const entriesByAlias = new Map();
|
|
71
|
-
for (const packageRoot of resolvePackageRootCandidates()) {
|
|
72
|
-
const packagesRoot = path.join(packageRoot, "packages");
|
|
73
|
-
if (!(await pathExists(packagesRoot))) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
const entries = await fs.readdir(packagesRoot, { withFileTypes: true });
|
|
77
|
-
for (const entry of entries) {
|
|
78
|
-
if (!entry.isDirectory() || !entry.name.startsWith("pm-")) {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
const candidateRoot = path.join(packagesRoot, entry.name);
|
|
82
|
-
if (!(await pathExists(path.join(candidateRoot, "package.json")))) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const manifest = await readPmPackageManifest(candidateRoot);
|
|
86
|
-
const aliases = manifest.aliases && manifest.aliases.length > 0
|
|
87
|
-
? manifest.aliases
|
|
88
|
-
: [derivePackageAlias(entry.name)];
|
|
89
|
-
for (const alias of aliases) {
|
|
90
|
-
const normalizedAlias = alias.trim().toLowerCase();
|
|
91
|
-
if (normalizedAlias.length === 0 || entriesByAlias.has(normalizedAlias)) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
entriesByAlias.set(normalizedAlias, {
|
|
95
|
-
alias: normalizedAlias,
|
|
96
|
-
package_directory: entry.name,
|
|
97
|
-
package_root: candidateRoot,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
for (const [alias, legacy] of Object.entries(LEGACY_BUNDLED_PACKAGE_ALIASES)) {
|
|
103
|
-
if (entriesByAlias.has(alias)) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
for (const packageRoot of resolvePackageRootCandidates()) {
|
|
107
|
-
const packagePath = path.join(packageRoot, "packages", legacy.package_directory);
|
|
108
|
-
if (await pathExists(path.join(packagePath, "package.json"))) {
|
|
109
|
-
entriesByAlias.set(alias, {
|
|
110
|
-
alias,
|
|
111
|
-
package_directory: legacy.package_directory,
|
|
112
|
-
package_root: packagePath,
|
|
113
|
-
});
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return [...entriesByAlias.values()].sort((left, right) => left.alias.localeCompare(right.alias));
|
|
119
|
-
}
|
|
120
|
-
async function listBundledPackageAliases() {
|
|
121
|
-
return (await collectBundledPackageEntries()).map((entry) => entry.alias);
|
|
122
|
-
}
|
|
123
|
-
async function resolveBundledPackageRoot(alias) {
|
|
124
|
-
const normalized = alias.trim().toLowerCase();
|
|
125
|
-
const entry = (await collectBundledPackageEntries()).find((candidate) => candidate.alias === normalized);
|
|
126
|
-
return entry?.package_root ?? null;
|
|
127
|
-
}
|
|
128
|
-
function normalizeStringList(values) {
|
|
129
|
-
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))].sort((left, right) => left.localeCompare(right));
|
|
130
|
-
}
|
|
131
|
-
function summarizePolicyWarnings(warnings) {
|
|
132
|
-
let warningCount = 0;
|
|
133
|
-
let violationCount = 0;
|
|
134
|
-
let blockedCount = 0;
|
|
135
|
-
for (const warning of warnings) {
|
|
136
|
-
if (!warning.startsWith("extension_policy_")) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
warningCount += 1;
|
|
140
|
-
if (warning.startsWith("extension_policy_violation_")) {
|
|
141
|
-
violationCount += 1;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (warning.startsWith("extension_policy_blocked_")) {
|
|
145
|
-
blockedCount += 1;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
warning_count: warningCount,
|
|
150
|
-
violation_count: violationCount,
|
|
151
|
-
blocked_count: blockedCount,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
30
|
+
const EXTENSION_INSTALL_COPY_ATTEMPTS = 3;
|
|
31
|
+
const EXTENSION_INSTALL_LOCK_ATTEMPTS = 120;
|
|
32
|
+
const EXTENSION_INSTALL_LOCK_DELAY_MS = 250;
|
|
33
|
+
const EXTENSION_INSTALL_LOCK_STALE_MS = 120_000;
|
|
154
34
|
function buildExtensionPolicyDetails(policy) {
|
|
155
35
|
const overrides = (policy.extension_overrides ?? [])
|
|
156
36
|
.map((override) => ({
|
|
@@ -209,263 +89,71 @@ function buildExtensionPolicyDetails(policy) {
|
|
|
209
89
|
})),
|
|
210
90
|
};
|
|
211
91
|
}
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
.toLowerCase()
|
|
216
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
217
|
-
.replace(/^-+|-+$/g, "");
|
|
218
|
-
if (normalized.length === 0) {
|
|
219
|
-
throw new PmCliError("Extension manifest name must resolve to a non-empty directory name.", EXIT_CODE.USAGE);
|
|
220
|
-
}
|
|
221
|
-
return normalized;
|
|
222
|
-
}
|
|
223
|
-
function parseExtensionManifest(raw) {
|
|
224
|
-
if (typeof raw !== "object" || raw === null) {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
const candidate = raw;
|
|
228
|
-
if (typeof candidate.name !== "string" || candidate.name.trim().length === 0) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
if (typeof candidate.version !== "string" || candidate.version.trim().length === 0) {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
if (typeof candidate.entry !== "string" || candidate.entry.trim().length === 0) {
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
let priority = DEFAULT_EXTENSION_PRIORITY;
|
|
238
|
-
if (candidate.priority !== undefined && candidate.priority !== null) {
|
|
239
|
-
if (typeof candidate.priority !== "number" || !Number.isInteger(candidate.priority)) {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
priority = candidate.priority;
|
|
243
|
-
}
|
|
244
|
-
let capabilities = [];
|
|
245
|
-
if (candidate.capabilities !== undefined && candidate.capabilities !== null) {
|
|
246
|
-
if (!Array.isArray(candidate.capabilities) || candidate.capabilities.some((value) => typeof value !== "string")) {
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
capabilities = normalizeStringList(candidate.capabilities.map((value) => String(value).toLowerCase()));
|
|
250
|
-
}
|
|
251
|
-
return {
|
|
252
|
-
name: candidate.name.trim(),
|
|
253
|
-
version: candidate.version.trim(),
|
|
254
|
-
entry: candidate.entry.trim(),
|
|
255
|
-
priority,
|
|
256
|
-
capabilities,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
async function isCanonicalPathWithinDirectory(directory, targetPath) {
|
|
260
|
-
const [resolvedDirectory, resolvedTargetPath] = await Promise.all([fs.realpath(directory), fs.realpath(targetPath)]);
|
|
261
|
-
return isPathWithinDirectory(resolvedDirectory, resolvedTargetPath);
|
|
262
|
-
}
|
|
263
|
-
async function validateExtensionDirectory(directory) {
|
|
264
|
-
const manifestPath = path.join(directory, "manifest.json");
|
|
265
|
-
if (!(await pathExists(manifestPath))) {
|
|
266
|
-
throw new PmCliError(`Extension manifest is missing at "${manifestPath}".`, EXIT_CODE.USAGE);
|
|
267
|
-
}
|
|
268
|
-
let parsedManifest;
|
|
269
|
-
try {
|
|
270
|
-
parsedManifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
271
|
-
}
|
|
272
|
-
catch (error) {
|
|
273
|
-
throw new PmCliError(`Failed to parse extension manifest at "${manifestPath}": ${error instanceof Error ? error.message : String(error)}`, EXIT_CODE.USAGE);
|
|
274
|
-
}
|
|
275
|
-
const manifest = parseExtensionManifest(parsedManifest);
|
|
276
|
-
if (!manifest) {
|
|
277
|
-
throw new PmCliError(`Extension manifest at "${manifestPath}" is invalid.`, EXIT_CODE.USAGE);
|
|
278
|
-
}
|
|
279
|
-
const entryPath = path.resolve(directory, manifest.entry);
|
|
280
|
-
if (!isPathWithinDirectory(directory, entryPath)) {
|
|
281
|
-
throw new PmCliError(`Extension entry "${manifest.entry}" resolves outside extension directory "${directory}".`, EXIT_CODE.USAGE);
|
|
282
|
-
}
|
|
283
|
-
if (!(await pathExists(entryPath))) {
|
|
284
|
-
throw new PmCliError(`Extension entry file is missing at "${entryPath}".`, EXIT_CODE.USAGE);
|
|
285
|
-
}
|
|
286
|
-
if (!(await isCanonicalPathWithinDirectory(directory, entryPath))) {
|
|
287
|
-
throw new PmCliError(`Extension entry "${manifest.entry}" resolves outside extension directory after symlink resolution.`, EXIT_CODE.USAGE);
|
|
92
|
+
function isRetriableExtensionInstallCopyError(error) {
|
|
93
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
94
|
+
return false;
|
|
288
95
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
manifest_path: manifestPath,
|
|
292
|
-
entry_path: entryPath,
|
|
293
|
-
manifest,
|
|
294
|
-
};
|
|
96
|
+
const code = error.code;
|
|
97
|
+
return code === "EEXIST" || code === "ENOTEMPTY" || code === "ENOENT";
|
|
295
98
|
}
|
|
296
|
-
|
|
297
|
-
return
|
|
99
|
+
function isErrnoCode(error, code) {
|
|
100
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
298
101
|
}
|
|
299
|
-
function
|
|
300
|
-
return {
|
|
301
|
-
|
|
302
|
-
updated_at: nowIso(),
|
|
303
|
-
entries: [],
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
function sortManagedEntries(entries) {
|
|
307
|
-
return [...entries].sort((left, right) => {
|
|
308
|
-
const byScope = left.scope.localeCompare(right.scope);
|
|
309
|
-
if (byScope !== 0) {
|
|
310
|
-
return byScope;
|
|
311
|
-
}
|
|
312
|
-
const byName = left.name.localeCompare(right.name);
|
|
313
|
-
if (byName !== 0) {
|
|
314
|
-
return byName;
|
|
315
|
-
}
|
|
316
|
-
return left.directory.localeCompare(right.directory);
|
|
102
|
+
function sleep(ms) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
setTimeout(resolve, ms);
|
|
317
105
|
});
|
|
318
106
|
}
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
if (left.kind === "builtin" && right.kind === "builtin") {
|
|
330
|
-
return left.name === right.name;
|
|
331
|
-
}
|
|
332
|
-
return true;
|
|
333
|
-
}
|
|
334
|
-
function normalizeManagedState(raw) {
|
|
335
|
-
if (typeof raw !== "object" || raw === null) {
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
const candidate = raw;
|
|
339
|
-
if (candidate.version !== MANAGED_EXTENSION_STATE_VERSION || !Array.isArray(candidate.entries)) {
|
|
340
|
-
return null;
|
|
341
|
-
}
|
|
342
|
-
const entries = [];
|
|
343
|
-
for (const rawEntry of candidate.entries) {
|
|
344
|
-
if (typeof rawEntry !== "object" || rawEntry === null) {
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
const entry = rawEntry;
|
|
348
|
-
if (typeof entry.name !== "string" ||
|
|
349
|
-
entry.name.trim().length === 0 ||
|
|
350
|
-
typeof entry.directory !== "string" ||
|
|
351
|
-
entry.directory.trim().length === 0 ||
|
|
352
|
-
(entry.scope !== "project" && entry.scope !== "global") ||
|
|
353
|
-
typeof entry.manifest_version !== "string" ||
|
|
354
|
-
typeof entry.manifest_entry !== "string" ||
|
|
355
|
-
!Array.isArray(entry.capabilities) ||
|
|
356
|
-
entry.capabilities.some((value) => typeof value !== "string") ||
|
|
357
|
-
typeof entry.installed_at !== "string" ||
|
|
358
|
-
typeof entry.updated_at !== "string" ||
|
|
359
|
-
typeof entry.source !== "object" ||
|
|
360
|
-
entry.source === null) {
|
|
361
|
-
continue;
|
|
107
|
+
export async function copyExtensionDirectoryForInstall(sourceDirectory, destinationDirectory, copyDirectory = fs.cp) {
|
|
108
|
+
let lastError = null;
|
|
109
|
+
for (let attempt = 1; attempt <= EXTENSION_INSTALL_COPY_ATTEMPTS; attempt += 1) {
|
|
110
|
+
try {
|
|
111
|
+
if (await pathExists(destinationDirectory)) {
|
|
112
|
+
await fs.rm(destinationDirectory, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
await copyDirectory(sourceDirectory, destinationDirectory, { recursive: true, force: true });
|
|
115
|
+
return;
|
|
362
116
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (!isRetriableExtensionInstallCopyError(error) || attempt === EXTENSION_INSTALL_COPY_ATTEMPTS) {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
lastError = error;
|
|
368
122
|
}
|
|
369
|
-
entries.push({
|
|
370
|
-
name: entry.name.trim(),
|
|
371
|
-
directory: entry.directory.trim(),
|
|
372
|
-
scope: entry.scope,
|
|
373
|
-
manifest_version: entry.manifest_version,
|
|
374
|
-
manifest_entry: entry.manifest_entry,
|
|
375
|
-
capabilities: normalizeStringList(entry.capabilities),
|
|
376
|
-
installed_at: entry.installed_at,
|
|
377
|
-
updated_at: entry.updated_at,
|
|
378
|
-
source: {
|
|
379
|
-
kind: source.kind,
|
|
380
|
-
input: source.input,
|
|
381
|
-
location: source.location,
|
|
382
|
-
name: typeof source.name === "string" ? source.name : undefined,
|
|
383
|
-
package: typeof source.package === "string" ? source.package : undefined,
|
|
384
|
-
version: typeof source.version === "string" ? source.version : undefined,
|
|
385
|
-
repository: typeof source.repository === "string" ? source.repository : undefined,
|
|
386
|
-
owner: typeof source.owner === "string" ? source.owner : undefined,
|
|
387
|
-
repo: typeof source.repo === "string" ? source.repo : undefined,
|
|
388
|
-
ref: typeof source.ref === "string" ? source.ref : undefined,
|
|
389
|
-
subpath: typeof source.subpath === "string" ? source.subpath : undefined,
|
|
390
|
-
commit: typeof source.commit === "string" ? source.commit : undefined,
|
|
391
|
-
},
|
|
392
|
-
last_update_check_at: typeof entry.last_update_check_at === "string" ? entry.last_update_check_at : undefined,
|
|
393
|
-
last_update_remote_commit: typeof entry.last_update_remote_commit === "string" ? entry.last_update_remote_commit : undefined,
|
|
394
|
-
update_available: typeof entry.update_available === "boolean" || entry.update_available === null
|
|
395
|
-
? entry.update_available
|
|
396
|
-
: undefined,
|
|
397
|
-
update_error: typeof entry.update_error === "string" ? entry.update_error : undefined,
|
|
398
|
-
});
|
|
399
123
|
}
|
|
400
|
-
return {
|
|
401
|
-
version: MANAGED_EXTENSION_STATE_VERSION,
|
|
402
|
-
updated_at: typeof candidate.updated_at === "string" ? candidate.updated_at : nowIso(),
|
|
403
|
-
entries: sortManagedEntries(entries),
|
|
404
|
-
};
|
|
405
124
|
}
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
return {
|
|
429
|
-
path: statePath,
|
|
430
|
-
state: fallback,
|
|
431
|
-
warnings: [],
|
|
432
|
-
};
|
|
125
|
+
async function withExtensionInstallLock(settingsRoot, destinationDirectoryName, run) {
|
|
126
|
+
const lockRoot = path.join(settingsRoot, "runtime", "extension-install-locks");
|
|
127
|
+
const lockPath = path.join(lockRoot, `${destinationDirectoryName}.lock`);
|
|
128
|
+
await fs.mkdir(lockRoot, { recursive: true });
|
|
129
|
+
let acquired = false;
|
|
130
|
+
for (let attempt = 1; attempt <= EXTENSION_INSTALL_LOCK_ATTEMPTS; attempt += 1) {
|
|
131
|
+
try {
|
|
132
|
+
await fs.mkdir(lockPath);
|
|
133
|
+
acquired = true;
|
|
134
|
+
await fs.writeFile(path.join(lockPath, "owner.json"), `${JSON.stringify({ pid: process.pid, created_at: nowIso(), destination: destinationDirectoryName }, null, 2)}\n`, "utf8");
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (!isErrnoCode(error, "EEXIST")) {
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
const stat = await fs.stat(lockPath).catch(() => null);
|
|
142
|
+
if (stat && Date.now() - stat.mtimeMs > EXTENSION_INSTALL_LOCK_STALE_MS) {
|
|
143
|
+
await fs.rm(lockPath, { recursive: true, force: true });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
await sleep(EXTENSION_INSTALL_LOCK_DELAY_MS);
|
|
433
147
|
}
|
|
434
|
-
return {
|
|
435
|
-
path: statePath,
|
|
436
|
-
state: fallback,
|
|
437
|
-
warnings: [`extension_manager_state_read_failed:${statePath}`],
|
|
438
|
-
};
|
|
439
148
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const statePath = resolveManagedExtensionStatePath(extensionsRoot);
|
|
443
|
-
const normalized = {
|
|
444
|
-
version: MANAGED_EXTENSION_STATE_VERSION,
|
|
445
|
-
updated_at: nowIso(),
|
|
446
|
-
entries: sortManagedEntries(state.entries),
|
|
447
|
-
};
|
|
448
|
-
await fs.mkdir(extensionsRoot, { recursive: true });
|
|
449
|
-
await fs.writeFile(statePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
450
|
-
}
|
|
451
|
-
function normalizeExtensionNameForMatch(value) {
|
|
452
|
-
return value.trim().toLowerCase();
|
|
453
|
-
}
|
|
454
|
-
async function resolveBundledAliasManifestName(input) {
|
|
455
|
-
const bundledAliasSource = await resolveBundledExtensionAliasSource(input);
|
|
456
|
-
if (!bundledAliasSource) {
|
|
457
|
-
return null;
|
|
149
|
+
if (!acquired) {
|
|
150
|
+
throw new PmCliError(`Timed out waiting for extension install lock for "${destinationDirectoryName}".`, EXIT_CODE.CONFLICT);
|
|
458
151
|
}
|
|
459
152
|
try {
|
|
460
|
-
|
|
461
|
-
if (extensionDirectories.length !== 1) {
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
const validated = await validateExtensionDirectory(extensionDirectories[0]);
|
|
465
|
-
return validated.manifest.name;
|
|
153
|
+
return await run();
|
|
466
154
|
}
|
|
467
|
-
|
|
468
|
-
|
|
155
|
+
finally {
|
|
156
|
+
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
|
|
469
157
|
}
|
|
470
158
|
}
|
|
471
159
|
async function resolveInstalledExtensionCandidate(installed, extensionTarget) {
|
|
@@ -596,505 +284,6 @@ function resolveScope(options) {
|
|
|
596
284
|
}
|
|
597
285
|
return global ? "global" : "project";
|
|
598
286
|
}
|
|
599
|
-
async function buildBundledPackageCatalog(scope, global, options = {}) {
|
|
600
|
-
const roots = resolveExtensionRoots(resolvePmRoot(process.cwd(), global.path), process.cwd());
|
|
601
|
-
const selectedRoot = scope === "global" ? roots.global : roots.project;
|
|
602
|
-
const managedStateRead = await readManagedExtensionState(selectedRoot);
|
|
603
|
-
const installedLocations = new Set(managedStateRead.state.entries
|
|
604
|
-
.filter((entry) => entry.scope === scope)
|
|
605
|
-
.filter((entry) => entry.source.kind !== "builtin")
|
|
606
|
-
.map((entry) => path.resolve(entry.source.location)));
|
|
607
|
-
const installedBuiltinAliases = new Set(managedStateRead.state.entries
|
|
608
|
-
.filter((entry) => entry.scope === scope && entry.source.kind === "builtin")
|
|
609
|
-
.flatMap((entry) => [entry.source.name, entry.source.input, entry.source.location])
|
|
610
|
-
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
611
|
-
.map((value) => value.trim().toLowerCase()));
|
|
612
|
-
const packages = [];
|
|
613
|
-
for (const alias of await listBundledPackageAliases()) {
|
|
614
|
-
const packageRoot = await resolveBundledPackageRoot(alias);
|
|
615
|
-
const installScopeFlag = scope === "global" ? "--global" : "--project";
|
|
616
|
-
if (!packageRoot) {
|
|
617
|
-
packages.push({
|
|
618
|
-
alias,
|
|
619
|
-
bundled: true,
|
|
620
|
-
available: false,
|
|
621
|
-
installed: false,
|
|
622
|
-
install_target: alias,
|
|
623
|
-
install_command: `pm install ${alias} ${installScopeFlag}`,
|
|
624
|
-
});
|
|
625
|
-
continue;
|
|
626
|
-
}
|
|
627
|
-
const manifest = await readPmPackageManifest(packageRoot);
|
|
628
|
-
const repository = manifest.catalog?.links?.repository ?? manifest.package_repository_url;
|
|
629
|
-
const report = manifest.catalog?.links?.report ?? manifest.package_bugs_url;
|
|
630
|
-
const docs = manifest.catalog?.links?.docs ?? manifest.package_homepage;
|
|
631
|
-
const npm = manifest.catalog?.links?.npm ??
|
|
632
|
-
(manifest.package_name && manifest.package_private !== true
|
|
633
|
-
? `https://www.npmjs.com/package/${encodeURIComponent(manifest.package_name)}`
|
|
634
|
-
: undefined);
|
|
635
|
-
const metadataOnlyResources = Object.fromEntries(PM_PACKAGE_RESOURCE_KINDS
|
|
636
|
-
.filter((resourceKind) => resourceKind !== "extensions")
|
|
637
|
-
.map((resourceKind) => [resourceKind, manifest.resources[resourceKind] ?? []])
|
|
638
|
-
.filter(([, entries]) => Array.isArray(entries) && entries.length > 0));
|
|
639
|
-
packages.push({
|
|
640
|
-
alias,
|
|
641
|
-
bundled: true,
|
|
642
|
-
available: true,
|
|
643
|
-
installed: installedBuiltinAliases.has(alias) || installedLocations.has(path.resolve(packageRoot)),
|
|
644
|
-
install_target: alias,
|
|
645
|
-
install_command: `pm install ${alias} ${installScopeFlag}`,
|
|
646
|
-
package_name: manifest.package_name,
|
|
647
|
-
package_version: manifest.package_version,
|
|
648
|
-
description: manifest.catalog?.summary ?? manifest.package_description,
|
|
649
|
-
keywords: manifest.package_keywords ?? [],
|
|
650
|
-
resources: manifest.resources,
|
|
651
|
-
installable_resources: {
|
|
652
|
-
extensions: manifest.resources.extensions ?? [],
|
|
653
|
-
},
|
|
654
|
-
metadata_only_resources: metadataOnlyResources,
|
|
655
|
-
catalog: {
|
|
656
|
-
display_name: manifest.catalog?.display_name,
|
|
657
|
-
category: manifest.catalog?.category,
|
|
658
|
-
tags: manifest.catalog?.tags ?? manifest.package_keywords ?? [],
|
|
659
|
-
links: {
|
|
660
|
-
docs,
|
|
661
|
-
npm,
|
|
662
|
-
repository,
|
|
663
|
-
report,
|
|
664
|
-
},
|
|
665
|
-
media: manifest.catalog?.media,
|
|
666
|
-
},
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
const fields = parsePackageCatalogFields(options.fields);
|
|
670
|
-
const outputPackages = fields ? packages.map((entry) => projectPackageCatalogEntry(entry, fields)) : packages;
|
|
671
|
-
return {
|
|
672
|
-
total: outputPackages.length,
|
|
673
|
-
scope,
|
|
674
|
-
installable_resource_kinds: ["extensions"],
|
|
675
|
-
metadata_only_resource_kinds: PM_PACKAGE_RESOURCE_KINDS.filter((resourceKind) => resourceKind !== "extensions"),
|
|
676
|
-
packages: outputPackages,
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
const PACKAGE_CATALOG_FIELD_KEYS = new Set([
|
|
680
|
-
"alias",
|
|
681
|
-
"bundled",
|
|
682
|
-
"available",
|
|
683
|
-
"installed",
|
|
684
|
-
"install_target",
|
|
685
|
-
"install_command",
|
|
686
|
-
"package_name",
|
|
687
|
-
"package_version",
|
|
688
|
-
"description",
|
|
689
|
-
"keywords",
|
|
690
|
-
"resources",
|
|
691
|
-
"installable_resources",
|
|
692
|
-
"metadata_only_resources",
|
|
693
|
-
"catalog",
|
|
694
|
-
"category",
|
|
695
|
-
"display_name",
|
|
696
|
-
]);
|
|
697
|
-
function parsePackageCatalogFields(raw) {
|
|
698
|
-
if (raw === undefined) {
|
|
699
|
-
return undefined;
|
|
700
|
-
}
|
|
701
|
-
const fields = [...new Set(raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0))];
|
|
702
|
-
if (fields.length === 0) {
|
|
703
|
-
throw new PmCliError("Package catalog --fields requires a comma-separated list of field names", EXIT_CODE.USAGE);
|
|
704
|
-
}
|
|
705
|
-
const unknown = fields.filter((field) => !PACKAGE_CATALOG_FIELD_KEYS.has(field));
|
|
706
|
-
if (unknown.length > 0) {
|
|
707
|
-
throw new PmCliError(`Unknown package catalog --fields value(s): ${unknown.join(", ")}`, EXIT_CODE.USAGE, {
|
|
708
|
-
examples: [
|
|
709
|
-
"pm package list --project --fields alias,installed,install_command",
|
|
710
|
-
"pm package catalog --project --fields alias,package_name,category",
|
|
711
|
-
],
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
return fields;
|
|
715
|
-
}
|
|
716
|
-
function projectPackageCatalogEntry(entry, fields) {
|
|
717
|
-
const projected = {};
|
|
718
|
-
for (const field of fields) {
|
|
719
|
-
if (field === "category") {
|
|
720
|
-
projected[field] = entry.catalog?.category ?? null;
|
|
721
|
-
}
|
|
722
|
-
else if (field === "display_name") {
|
|
723
|
-
projected[field] = entry.catalog?.display_name ?? null;
|
|
724
|
-
}
|
|
725
|
-
else {
|
|
726
|
-
projected[field] = entry[field] ?? null;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return projected;
|
|
730
|
-
}
|
|
731
|
-
function parseGithubPathSpec(pathSpec, input, refOverride) {
|
|
732
|
-
const segments = pathSpec
|
|
733
|
-
.split("/")
|
|
734
|
-
.map((segment) => segment.trim())
|
|
735
|
-
.filter((segment) => segment.length > 0);
|
|
736
|
-
if (segments.length < 2) {
|
|
737
|
-
return null;
|
|
738
|
-
}
|
|
739
|
-
const owner = segments[0];
|
|
740
|
-
const repo = segments[1].replace(/\.git$/i, "");
|
|
741
|
-
if (owner.length === 0 || repo.length === 0) {
|
|
742
|
-
return null;
|
|
743
|
-
}
|
|
744
|
-
const tail = segments.slice(2);
|
|
745
|
-
let ref;
|
|
746
|
-
let subpath;
|
|
747
|
-
if (tail[0] === "tree" && tail.length >= 2) {
|
|
748
|
-
ref = tail[1];
|
|
749
|
-
subpath = tail.slice(2).join("/");
|
|
750
|
-
}
|
|
751
|
-
else if (tail.length > 0) {
|
|
752
|
-
subpath = tail.join("/");
|
|
753
|
-
}
|
|
754
|
-
if (typeof refOverride === "string" && refOverride.trim().length > 0) {
|
|
755
|
-
ref = refOverride.trim();
|
|
756
|
-
}
|
|
757
|
-
return {
|
|
758
|
-
kind: "github",
|
|
759
|
-
input,
|
|
760
|
-
owner,
|
|
761
|
-
repo,
|
|
762
|
-
repository: `https://github.com/${owner}/${repo}.git`,
|
|
763
|
-
ref,
|
|
764
|
-
subpath: subpath && subpath.length > 0 ? subpath : undefined,
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
export function parseExtensionInstallSource(input, options = {}) {
|
|
768
|
-
const normalizedInput = input.trim();
|
|
769
|
-
if (normalizedInput.length === 0) {
|
|
770
|
-
throw new PmCliError("Extension source is required for --install.", EXIT_CODE.USAGE);
|
|
771
|
-
}
|
|
772
|
-
const refOverride = typeof options.ref === "string" && options.ref.trim().length > 0 ? options.ref.trim() : undefined;
|
|
773
|
-
if (normalizedInput.startsWith("npm:")) {
|
|
774
|
-
const spec = normalizedInput.slice("npm:".length).trim();
|
|
775
|
-
if (spec.length === 0) {
|
|
776
|
-
throw new PmCliError('npm package source must include a package spec after "npm:".', EXIT_CODE.USAGE);
|
|
777
|
-
}
|
|
778
|
-
if (options.forceGithub) {
|
|
779
|
-
throw new PmCliError('Options "--gh/--github" cannot be combined with npm: package sources.', EXIT_CODE.USAGE);
|
|
780
|
-
}
|
|
781
|
-
if (refOverride) {
|
|
782
|
-
throw new PmCliError('Option "--ref" cannot be combined with npm: package sources.', EXIT_CODE.USAGE);
|
|
783
|
-
}
|
|
784
|
-
return {
|
|
785
|
-
kind: "npm",
|
|
786
|
-
input: normalizedInput,
|
|
787
|
-
spec,
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
const maybeGithubByUrl = (() => {
|
|
791
|
-
try {
|
|
792
|
-
const parsed = new URL(normalizedInput);
|
|
793
|
-
if (parsed.hostname !== "github.com") {
|
|
794
|
-
return null;
|
|
795
|
-
}
|
|
796
|
-
const pathSpec = parsed.pathname.replace(/^\/+/, "");
|
|
797
|
-
return parseGithubPathSpec(pathSpec, normalizedInput, refOverride);
|
|
798
|
-
}
|
|
799
|
-
catch {
|
|
800
|
-
return null;
|
|
801
|
-
}
|
|
802
|
-
})();
|
|
803
|
-
if (maybeGithubByUrl) {
|
|
804
|
-
return maybeGithubByUrl;
|
|
805
|
-
}
|
|
806
|
-
const strippedDomainInput = normalizedInput.startsWith("github.com/") ? normalizedInput.slice("github.com/".length) : null;
|
|
807
|
-
if (strippedDomainInput) {
|
|
808
|
-
const parsed = parseGithubPathSpec(strippedDomainInput, normalizedInput, refOverride);
|
|
809
|
-
if (!parsed) {
|
|
810
|
-
throw new PmCliError(`Invalid GitHub source "${normalizedInput}".`, EXIT_CODE.USAGE);
|
|
811
|
-
}
|
|
812
|
-
return parsed;
|
|
813
|
-
}
|
|
814
|
-
if (options.forceGithub) {
|
|
815
|
-
const parsed = parseGithubPathSpec(normalizedInput, normalizedInput, refOverride);
|
|
816
|
-
if (!parsed) {
|
|
817
|
-
throw new PmCliError(`Invalid GitHub shorthand "${normalizedInput}".`, EXIT_CODE.USAGE);
|
|
818
|
-
}
|
|
819
|
-
return parsed;
|
|
820
|
-
}
|
|
821
|
-
if (/^https?:\/\//i.test(normalizedInput)) {
|
|
822
|
-
throw new PmCliError(`Unsupported extension source URL "${normalizedInput}". Supported remote source host: github.com.`, EXIT_CODE.USAGE);
|
|
823
|
-
}
|
|
824
|
-
return {
|
|
825
|
-
kind: "local",
|
|
826
|
-
input: normalizedInput,
|
|
827
|
-
absolute_path: path.resolve(process.cwd(), normalizedInput),
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
async function runGitCommand(args) {
|
|
831
|
-
try {
|
|
832
|
-
const result = await execFileAsync("git", args, { encoding: "utf8" });
|
|
833
|
-
return (result.stdout ?? "").trim();
|
|
834
|
-
}
|
|
835
|
-
catch (error) {
|
|
836
|
-
const stderr = typeof error === "object" && error !== null && "stderr" in error ? String(error.stderr) : "";
|
|
837
|
-
const message = stderr.trim().length > 0 ? stderr.trim() : error instanceof Error ? error.message : String(error);
|
|
838
|
-
throw new PmCliError(`Git command failed: git ${args.join(" ")}\n${message}`, EXIT_CODE.GENERIC_FAILURE);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
async function runNpmCommand(args, cwd) {
|
|
842
|
-
try {
|
|
843
|
-
const result = await execFileAsync("npm", args, { cwd, encoding: "utf8" });
|
|
844
|
-
return (result.stdout ?? "").trim();
|
|
845
|
-
}
|
|
846
|
-
catch (error) {
|
|
847
|
-
const stderr = typeof error === "object" && error !== null && "stderr" in error ? String(error.stderr) : "";
|
|
848
|
-
const message = stderr.trim().length > 0 ? stderr.trim() : error instanceof Error ? error.message : String(error);
|
|
849
|
-
throw new PmCliError(`npm command failed: npm ${args.join(" ")}\n${message}`, EXIT_CODE.GENERIC_FAILURE);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
async function resolveLocalNpmPackagePath(spec) {
|
|
853
|
-
if (path.isAbsolute(spec) || spec.startsWith(".") || spec.startsWith("..")) {
|
|
854
|
-
const absolutePath = path.resolve(process.cwd(), spec);
|
|
855
|
-
return (await pathExists(absolutePath)) ? absolutePath : null;
|
|
856
|
-
}
|
|
857
|
-
try {
|
|
858
|
-
const parsed = new URL(spec);
|
|
859
|
-
if (parsed.protocol === "file:") {
|
|
860
|
-
const absolutePath = fileURLToPath(parsed);
|
|
861
|
-
return (await pathExists(absolutePath)) ? absolutePath : null;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
catch {
|
|
865
|
-
// Registry package specs are not URLs.
|
|
866
|
-
}
|
|
867
|
-
return null;
|
|
868
|
-
}
|
|
869
|
-
async function resolveNpmPackSpec(spec) {
|
|
870
|
-
const localPath = await resolveLocalNpmPackagePath(spec);
|
|
871
|
-
if (localPath) {
|
|
872
|
-
return pathToFileURL(localPath).href;
|
|
873
|
-
}
|
|
874
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(spec)) {
|
|
875
|
-
return spec;
|
|
876
|
-
}
|
|
877
|
-
return spec;
|
|
878
|
-
}
|
|
879
|
-
function parsePackedNpmPackage(stdout, packDirectory) {
|
|
880
|
-
try {
|
|
881
|
-
const parsed = JSON.parse(stdout);
|
|
882
|
-
const first = Array.isArray(parsed) ? parsed[0] : undefined;
|
|
883
|
-
if (first && typeof first.filename === "string" && first.filename.trim().length > 0) {
|
|
884
|
-
return {
|
|
885
|
-
tarball: path.resolve(packDirectory, first.filename),
|
|
886
|
-
package: typeof first.name === "string" ? first.name : undefined,
|
|
887
|
-
version: typeof first.version === "string" ? first.version : undefined,
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
catch {
|
|
892
|
-
// Fall back to the last stdout line for older npm output.
|
|
893
|
-
}
|
|
894
|
-
const lastLine = stdout
|
|
895
|
-
.split(/\r?\n/)
|
|
896
|
-
.map((line) => line.trim())
|
|
897
|
-
.filter((line) => line.length > 0)
|
|
898
|
-
.at(-1);
|
|
899
|
-
if (!lastLine) {
|
|
900
|
-
throw new PmCliError("npm pack did not report a tarball filename.", EXIT_CODE.GENERIC_FAILURE);
|
|
901
|
-
}
|
|
902
|
-
return {
|
|
903
|
-
tarball: path.resolve(packDirectory, lastLine),
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
async function resolveNpmSourceDirectory(source) {
|
|
907
|
-
const localPackageRoot = await resolveLocalNpmPackagePath(source.spec);
|
|
908
|
-
if (localPackageRoot) {
|
|
909
|
-
const packageJsonPath = path.join(localPackageRoot, "package.json");
|
|
910
|
-
const packageJson = (await pathExists(packageJsonPath))
|
|
911
|
-
? JSON.parse(await fs.readFile(packageJsonPath, "utf8"))
|
|
912
|
-
: {};
|
|
913
|
-
return {
|
|
914
|
-
directory: await resolvePackageExtensionDirectory(localPackageRoot, source.input),
|
|
915
|
-
package: typeof packageJson.name === "string" ? packageJson.name : undefined,
|
|
916
|
-
version: typeof packageJson.version === "string" ? packageJson.version : undefined,
|
|
917
|
-
cleanup: async () => { },
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pm-npm-package-source-"));
|
|
921
|
-
const packDirectory = path.join(tempRoot, "pack");
|
|
922
|
-
const extractDirectory = path.join(tempRoot, "extract");
|
|
923
|
-
await fs.mkdir(packDirectory, { recursive: true });
|
|
924
|
-
await fs.mkdir(extractDirectory, { recursive: true });
|
|
925
|
-
try {
|
|
926
|
-
const packSpec = await resolveNpmPackSpec(source.spec);
|
|
927
|
-
const packStdout = await runNpmCommand(["pack", packSpec, "--json", "--pack-destination", packDirectory]);
|
|
928
|
-
const packed = parsePackedNpmPackage(packStdout, packDirectory);
|
|
929
|
-
await execFileAsync("tar", ["-xzf", packed.tarball, "-C", extractDirectory], { encoding: "utf8" });
|
|
930
|
-
const packageRoot = path.join(extractDirectory, "package");
|
|
931
|
-
await installNpmPackageRuntimeDependencies(packageRoot);
|
|
932
|
-
const directory = await resolvePackageExtensionDirectory(packageRoot, source.input);
|
|
933
|
-
return {
|
|
934
|
-
directory,
|
|
935
|
-
package: packed.package,
|
|
936
|
-
version: packed.version,
|
|
937
|
-
cleanup: async () => {
|
|
938
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
939
|
-
},
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
catch (error) {
|
|
943
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
944
|
-
throw error;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
async function installNpmPackageRuntimeDependencies(packageRoot) {
|
|
948
|
-
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
949
|
-
if (!(await pathExists(packageJsonPath))) {
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
let parsed;
|
|
953
|
-
try {
|
|
954
|
-
parsed = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
955
|
-
}
|
|
956
|
-
catch {
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
const manifest = parsed;
|
|
963
|
-
const dependencySpecs = runtimeDependencyInstallSpecs(manifest);
|
|
964
|
-
if (dependencySpecs.length === 0) {
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
const runtimeOnlyManifest = { ...parsed };
|
|
968
|
-
delete runtimeOnlyManifest.devDependencies;
|
|
969
|
-
await fs.writeFile(packageJsonPath, `${JSON.stringify(runtimeOnlyManifest, null, 2)}\n`, "utf8");
|
|
970
|
-
await Promise.all([
|
|
971
|
-
fs.rm(path.join(packageRoot, "package-lock.json"), { force: true }),
|
|
972
|
-
fs.rm(path.join(packageRoot, "npm-shrinkwrap.json"), { force: true }),
|
|
973
|
-
]);
|
|
974
|
-
await runNpmCommand(["install", "--ignore-scripts", "--no-audit", "--fund=false", "--package-lock=false", "--no-save", ...dependencySpecs], packageRoot);
|
|
975
|
-
}
|
|
976
|
-
function runtimeDependencyInstallSpecs(manifest) {
|
|
977
|
-
const specs = new Map();
|
|
978
|
-
for (const dependencyMap of [manifest.dependencies, manifest.optionalDependencies, manifest.peerDependencies]) {
|
|
979
|
-
if (typeof dependencyMap !== "object" || dependencyMap === null) {
|
|
980
|
-
continue;
|
|
981
|
-
}
|
|
982
|
-
for (const [name, version] of Object.entries(dependencyMap)) {
|
|
983
|
-
if (typeof version !== "string" || version.trim().length === 0 || specs.has(name)) {
|
|
984
|
-
continue;
|
|
985
|
-
}
|
|
986
|
-
specs.set(name, `${name}@${version.trim()}`);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
return [...specs.values()];
|
|
990
|
-
}
|
|
991
|
-
async function resolvePackageExtensionDirectory(packageRoot, sourceLabel) {
|
|
992
|
-
const discovered = await collectPackageExtensionDirectories(packageRoot);
|
|
993
|
-
if (discovered.length === 1) {
|
|
994
|
-
return discovered[0];
|
|
995
|
-
}
|
|
996
|
-
if (discovered.length > 1) {
|
|
997
|
-
const choices = discovered
|
|
998
|
-
.map((entry) => path.relative(packageRoot, entry).replaceAll(path.sep, "/"))
|
|
999
|
-
.sort((left, right) => left.localeCompare(right));
|
|
1000
|
-
throw new PmCliError(`Package source "${sourceLabel}" contains multiple extension manifests. Provide an explicit extension path. Candidates: ${choices.join(", ")}`, EXIT_CODE.USAGE);
|
|
1001
|
-
}
|
|
1002
|
-
throw new PmCliError(`Unable to locate a pm extension manifest in package source "${sourceLabel}". Package installs currently activate only extension resources, so add package.json pm.extensions or an extensions/ directory. Metadata-only resources like pm.docs/pm.examples are catalog metadata and do not activate commands.`, EXIT_CODE.USAGE);
|
|
1003
|
-
}
|
|
1004
|
-
async function resolveGithubSourceDirectory(cloneDirectory, source) {
|
|
1005
|
-
const candidatePaths = [];
|
|
1006
|
-
if (source.subpath) {
|
|
1007
|
-
candidatePaths.push(source.subpath);
|
|
1008
|
-
candidatePaths.push(path.posix.join(".agents/pm/extensions", source.subpath));
|
|
1009
|
-
candidatePaths.push(path.posix.join(".custom/pm-extensions", source.subpath));
|
|
1010
|
-
candidatePaths.push(path.posix.join(".custom/pm-extension", source.subpath));
|
|
1011
|
-
}
|
|
1012
|
-
for (const candidate of candidatePaths) {
|
|
1013
|
-
const absolute = path.resolve(cloneDirectory, candidate);
|
|
1014
|
-
if (await pathExists(path.join(absolute, "manifest.json"))) {
|
|
1015
|
-
return { directory: absolute, resolved_subpath: candidate };
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
if (await pathExists(path.join(cloneDirectory, "manifest.json"))) {
|
|
1019
|
-
return { directory: cloneDirectory, resolved_subpath: "." };
|
|
1020
|
-
}
|
|
1021
|
-
const discoveredDirectory = await resolvePackageExtensionDirectory(cloneDirectory, source.input);
|
|
1022
|
-
return {
|
|
1023
|
-
directory: discoveredDirectory,
|
|
1024
|
-
resolved_subpath: path.relative(cloneDirectory, discoveredDirectory).replaceAll(path.sep, "/"),
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
async function resolveInstallSource(source) {
|
|
1028
|
-
if (source.kind === "local") {
|
|
1029
|
-
let localStats;
|
|
1030
|
-
try {
|
|
1031
|
-
localStats = await fs.stat(source.absolute_path);
|
|
1032
|
-
}
|
|
1033
|
-
catch {
|
|
1034
|
-
throw new PmCliError(`Local extension source does not exist: "${source.absolute_path}".`, EXIT_CODE.NOT_FOUND);
|
|
1035
|
-
}
|
|
1036
|
-
if (!localStats.isDirectory()) {
|
|
1037
|
-
throw new PmCliError(`Local extension source must be a directory: "${source.absolute_path}".`, EXIT_CODE.USAGE);
|
|
1038
|
-
}
|
|
1039
|
-
const directory = await resolvePackageExtensionDirectory(source.absolute_path, source.input);
|
|
1040
|
-
return {
|
|
1041
|
-
source,
|
|
1042
|
-
directory,
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
if (source.kind === "npm") {
|
|
1046
|
-
const resolved = await resolveNpmSourceDirectory(source);
|
|
1047
|
-
return {
|
|
1048
|
-
source,
|
|
1049
|
-
directory: resolved.directory,
|
|
1050
|
-
cleanup: resolved.cleanup,
|
|
1051
|
-
resolved_subpath: path.relative(path.dirname(resolved.directory), resolved.directory).replaceAll(path.sep, "/"),
|
|
1052
|
-
npm_package: resolved.package,
|
|
1053
|
-
npm_version: resolved.version,
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
const cloneDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "pm-extension-source-"));
|
|
1057
|
-
const cloneArgs = ["clone", "--depth", "1"];
|
|
1058
|
-
if (source.ref) {
|
|
1059
|
-
cloneArgs.push("--branch", source.ref);
|
|
1060
|
-
}
|
|
1061
|
-
cloneArgs.push(source.repository, cloneDirectory);
|
|
1062
|
-
try {
|
|
1063
|
-
await runGitCommand(cloneArgs);
|
|
1064
|
-
const commit = await runGitCommand(["-C", cloneDirectory, "rev-parse", "HEAD"]);
|
|
1065
|
-
const resolved = await resolveGithubSourceDirectory(cloneDirectory, source);
|
|
1066
|
-
return {
|
|
1067
|
-
source,
|
|
1068
|
-
directory: resolved.directory,
|
|
1069
|
-
resolved_subpath: resolved.resolved_subpath,
|
|
1070
|
-
commit,
|
|
1071
|
-
cleanup: async () => {
|
|
1072
|
-
await fs.rm(cloneDirectory, { recursive: true, force: true });
|
|
1073
|
-
},
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
catch (error) {
|
|
1077
|
-
await fs.rm(cloneDirectory, { recursive: true, force: true });
|
|
1078
|
-
throw error;
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
async function areDirectoriesEquivalent(left, right) {
|
|
1082
|
-
if (!(await pathExists(left)) || !(await pathExists(right))) {
|
|
1083
|
-
return false;
|
|
1084
|
-
}
|
|
1085
|
-
const [leftRealPath, rightRealPath] = await Promise.all([fs.realpath(left), fs.realpath(right)]);
|
|
1086
|
-
return leftRealPath === rightRealPath;
|
|
1087
|
-
}
|
|
1088
|
-
function upsertManagedEntry(state, entry) {
|
|
1089
|
-
const updatedEntries = state.entries.filter((candidate) => normalizeExtensionNameForMatch(candidate.name) !== normalizeExtensionNameForMatch(entry.name) &&
|
|
1090
|
-
normalizeExtensionNameForMatch(candidate.directory) !== normalizeExtensionNameForMatch(entry.directory));
|
|
1091
|
-
updatedEntries.push(entry);
|
|
1092
|
-
return {
|
|
1093
|
-
...state,
|
|
1094
|
-
updated_at: nowIso(),
|
|
1095
|
-
entries: sortManagedEntries(updatedEntries),
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
287
|
function resolveUpdateCheckResolution(managedEntry) {
|
|
1099
288
|
if (!managedEntry) {
|
|
1100
289
|
return {
|
|
@@ -1229,88 +418,6 @@ async function listInstalledExtensions(extensionsRoot, scope, settings, state) {
|
|
|
1229
418
|
warnings: warnings.sort((left, right) => left.localeCompare(right)),
|
|
1230
419
|
};
|
|
1231
420
|
}
|
|
1232
|
-
function applyDoctorRuntimeActivationState(extensions, loadResult, activationResult) {
|
|
1233
|
-
const loadedNames = new Set(loadResult.loaded.map((entry) => normalizeExtensionNameForMatch(entry.name)));
|
|
1234
|
-
const loadFailedNames = new Set(loadResult.failed.map((entry) => normalizeExtensionNameForMatch(entry.name)));
|
|
1235
|
-
const activationFailedNames = new Set(activationResult.failed.map((entry) => normalizeExtensionNameForMatch(entry.name)));
|
|
1236
|
-
const commandPathsByExtension = new Map();
|
|
1237
|
-
const actionPathsByExtension = new Map();
|
|
1238
|
-
const addCommandPath = (extensionName, commandPath) => {
|
|
1239
|
-
const normalizedName = normalizeExtensionNameForMatch(extensionName);
|
|
1240
|
-
const normalizedCommandPath = commandPath.trim();
|
|
1241
|
-
if (normalizedName.length === 0 || normalizedCommandPath.length === 0) {
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
const existing = commandPathsByExtension.get(normalizedName) ?? new Set();
|
|
1245
|
-
existing.add(normalizedCommandPath);
|
|
1246
|
-
commandPathsByExtension.set(normalizedName, existing);
|
|
1247
|
-
};
|
|
1248
|
-
const addActionPath = (extensionName, actionPath) => {
|
|
1249
|
-
const normalizedName = normalizeExtensionNameForMatch(extensionName);
|
|
1250
|
-
const normalizedActionPath = actionPath.trim();
|
|
1251
|
-
if (normalizedName.length === 0 || normalizedActionPath.length === 0) {
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
const existing = actionPathsByExtension.get(normalizedName) ?? new Set();
|
|
1255
|
-
existing.add(normalizedActionPath);
|
|
1256
|
-
actionPathsByExtension.set(normalizedName, existing);
|
|
1257
|
-
};
|
|
1258
|
-
for (const registration of activationResult.registrations.commands) {
|
|
1259
|
-
addCommandPath(registration.name, registration.command);
|
|
1260
|
-
addActionPath(registration.name, registration.action);
|
|
1261
|
-
}
|
|
1262
|
-
for (const handler of activationResult.commands.handlers) {
|
|
1263
|
-
addCommandPath(handler.name, handler.command);
|
|
1264
|
-
}
|
|
1265
|
-
for (const override of activationResult.commands.overrides) {
|
|
1266
|
-
addCommandPath(override.name, override.command);
|
|
1267
|
-
}
|
|
1268
|
-
const sortedPaths = (values) => {
|
|
1269
|
-
if (!values || values.size === 0) {
|
|
1270
|
-
return undefined;
|
|
1271
|
-
}
|
|
1272
|
-
return [...values].sort((left, right) => left.localeCompare(right));
|
|
1273
|
-
};
|
|
1274
|
-
return extensions.map((entry) => {
|
|
1275
|
-
const normalizedName = normalizeExtensionNameForMatch(entry.name);
|
|
1276
|
-
const commandPaths = sortedPaths(commandPathsByExtension.get(normalizedName));
|
|
1277
|
-
const actionPaths = sortedPaths(actionPathsByExtension.get(normalizedName));
|
|
1278
|
-
const runtimeMetadata = {
|
|
1279
|
-
...(commandPaths ? { command_paths: commandPaths } : {}),
|
|
1280
|
-
...(actionPaths ? { action_paths: actionPaths } : {}),
|
|
1281
|
-
};
|
|
1282
|
-
if (!entry.enabled) {
|
|
1283
|
-
return {
|
|
1284
|
-
...entry,
|
|
1285
|
-
runtime_active: false,
|
|
1286
|
-
activation_status: "not_loaded",
|
|
1287
|
-
...runtimeMetadata,
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
if (loadFailedNames.has(normalizedName) || activationFailedNames.has(normalizedName)) {
|
|
1291
|
-
return {
|
|
1292
|
-
...entry,
|
|
1293
|
-
runtime_active: false,
|
|
1294
|
-
activation_status: "failed",
|
|
1295
|
-
...runtimeMetadata,
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
if (loadedNames.has(normalizedName)) {
|
|
1299
|
-
return {
|
|
1300
|
-
...entry,
|
|
1301
|
-
runtime_active: true,
|
|
1302
|
-
activation_status: "ok",
|
|
1303
|
-
...runtimeMetadata,
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
return {
|
|
1307
|
-
...entry,
|
|
1308
|
-
runtime_active: false,
|
|
1309
|
-
activation_status: "not_loaded",
|
|
1310
|
-
...runtimeMetadata,
|
|
1311
|
-
};
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
421
|
async function checkGithubUpdate(source) {
|
|
1315
422
|
const checkedAt = nowIso();
|
|
1316
423
|
if (source.kind !== "github" || !source.repository) {
|
|
@@ -1448,391 +555,6 @@ function requireTarget(target, action) {
|
|
|
1448
555
|
}
|
|
1449
556
|
return normalized;
|
|
1450
557
|
}
|
|
1451
|
-
function lifecycleFlagCommand(options, action) {
|
|
1452
|
-
return options.vocabulary === "package" ? `pm package ${action}` : `pm extension --${action}`;
|
|
1453
|
-
}
|
|
1454
|
-
function buildStarterExtensionScaffoldFiles(extensionName, commandName, vocabulary) {
|
|
1455
|
-
const packageName = `pm-${extensionName}`;
|
|
1456
|
-
const manifest = `${JSON.stringify({
|
|
1457
|
-
name: extensionName,
|
|
1458
|
-
version: "0.1.0",
|
|
1459
|
-
entry: "./index.js",
|
|
1460
|
-
capabilities: ["commands"],
|
|
1461
|
-
}, null, 2)}\n`;
|
|
1462
|
-
const entrypoint = [
|
|
1463
|
-
"export function activate(api) {",
|
|
1464
|
-
" api.registerCommand({",
|
|
1465
|
-
` name: ${JSON.stringify(commandName)},`,
|
|
1466
|
-
' description: "Starter scaffold command. Replace with your own behavior.",',
|
|
1467
|
-
" run: async (context) => ({",
|
|
1468
|
-
" ok: true,",
|
|
1469
|
-
` source: ${JSON.stringify(extensionName)},`,
|
|
1470
|
-
" command: context.command,",
|
|
1471
|
-
' message: "Starter extension scaffold is active.",',
|
|
1472
|
-
" }),",
|
|
1473
|
-
" });",
|
|
1474
|
-
"}",
|
|
1475
|
-
"",
|
|
1476
|
-
"export default {",
|
|
1477
|
-
" activate,",
|
|
1478
|
-
"};",
|
|
1479
|
-
"",
|
|
1480
|
-
].join("\n");
|
|
1481
|
-
if (vocabulary === "package") {
|
|
1482
|
-
const packageJson = `${JSON.stringify({
|
|
1483
|
-
name: packageName,
|
|
1484
|
-
version: "0.1.0",
|
|
1485
|
-
private: true,
|
|
1486
|
-
type: "module",
|
|
1487
|
-
keywords: ["pm-package"],
|
|
1488
|
-
peerDependencies: {
|
|
1489
|
-
"@unbrained/pm-cli": "*",
|
|
1490
|
-
},
|
|
1491
|
-
pm: {
|
|
1492
|
-
aliases: [extensionName],
|
|
1493
|
-
extensions: [`extensions/${extensionName}`],
|
|
1494
|
-
docs: ["README.md"],
|
|
1495
|
-
examples: ["README.md"],
|
|
1496
|
-
catalog: {
|
|
1497
|
-
display_name: extensionName,
|
|
1498
|
-
category: "workflow",
|
|
1499
|
-
summary: "Starter pm package scaffold.",
|
|
1500
|
-
tags: ["starter"],
|
|
1501
|
-
},
|
|
1502
|
-
},
|
|
1503
|
-
}, null, 2)}\n`;
|
|
1504
|
-
const packageReadme = [
|
|
1505
|
-
`# ${packageName}`,
|
|
1506
|
-
"",
|
|
1507
|
-
"Generated by `pm package init`.",
|
|
1508
|
-
"",
|
|
1509
|
-
"## Included Files",
|
|
1510
|
-
"- `package.json`: package metadata and `pm` resource manifest.",
|
|
1511
|
-
`- \`extensions/${extensionName}/manifest.json\`: extension metadata and capabilities.`,
|
|
1512
|
-
`- \`extensions/${extensionName}/index.js\`: starter command registration using the \`commands\` capability.`,
|
|
1513
|
-
"",
|
|
1514
|
-
"## Quick Start",
|
|
1515
|
-
"```bash",
|
|
1516
|
-
"pm install --project <package-path>",
|
|
1517
|
-
`pm ${commandName}`,
|
|
1518
|
-
"pm package doctor --project --detail summary",
|
|
1519
|
-
"```",
|
|
1520
|
-
"",
|
|
1521
|
-
"## Notes",
|
|
1522
|
-
"- Keep package metadata in `package.json` and runtime behavior under `extensions/`.",
|
|
1523
|
-
"- Add capabilities to the extension manifest only when the entrypoint uses the matching SDK API.",
|
|
1524
|
-
"- Use `@unbrained/pm-cli/sdk` as the public SDK import for richer package runtimes.",
|
|
1525
|
-
"",
|
|
1526
|
-
].join("\n");
|
|
1527
|
-
return {
|
|
1528
|
-
"package.json": packageJson,
|
|
1529
|
-
[`extensions/${extensionName}/manifest.json`]: manifest,
|
|
1530
|
-
[`extensions/${extensionName}/index.js`]: entrypoint,
|
|
1531
|
-
"README.md": packageReadme,
|
|
1532
|
-
};
|
|
1533
|
-
}
|
|
1534
|
-
const readme = [
|
|
1535
|
-
`# ${extensionName}`,
|
|
1536
|
-
"",
|
|
1537
|
-
"Generated by `pm extension init`.",
|
|
1538
|
-
"",
|
|
1539
|
-
"## Included Files",
|
|
1540
|
-
"- `manifest.json`: extension metadata and capabilities.",
|
|
1541
|
-
"- `index.js`: starter command registration using the `commands` capability.",
|
|
1542
|
-
"",
|
|
1543
|
-
"## Quick Start",
|
|
1544
|
-
"```bash",
|
|
1545
|
-
"pm extension --install --project <scaffold-path>",
|
|
1546
|
-
`pm ${commandName}`,
|
|
1547
|
-
"pm extension --doctor --project --detail summary",
|
|
1548
|
-
"```",
|
|
1549
|
-
"",
|
|
1550
|
-
"## Notes",
|
|
1551
|
-
"- This scaffold uses ESM exports so it works in package scopes with `type: module`.",
|
|
1552
|
-
"- Update `manifest.json` capabilities and `index.js` command behavior as your extension evolves.",
|
|
1553
|
-
"",
|
|
1554
|
-
].join("\n");
|
|
1555
|
-
return {
|
|
1556
|
-
"manifest.json": manifest,
|
|
1557
|
-
"index.js": entrypoint,
|
|
1558
|
-
"README.md": readme,
|
|
1559
|
-
};
|
|
1560
|
-
}
|
|
1561
|
-
async function scaffoldExtensionProject(target, vocabulary = "extension") {
|
|
1562
|
-
const normalizedTarget = target.trim();
|
|
1563
|
-
const targetPath = path.resolve(process.cwd(), normalizedTarget);
|
|
1564
|
-
const extensionName = normalizeManagedDirectoryName(path.basename(targetPath));
|
|
1565
|
-
const commandName = `${extensionName} ping`;
|
|
1566
|
-
const scaffoldFiles = buildStarterExtensionScaffoldFiles(extensionName, commandName, vocabulary);
|
|
1567
|
-
let createdDirectory = false;
|
|
1568
|
-
if (await pathExists(targetPath)) {
|
|
1569
|
-
const existingTargetStats = await fs.stat(targetPath);
|
|
1570
|
-
if (!existingTargetStats.isDirectory()) {
|
|
1571
|
-
throw new PmCliError(`Scaffold target "${targetPath}" exists and is not a directory.`, EXIT_CODE.CONFLICT);
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
else {
|
|
1575
|
-
await fs.mkdir(targetPath, { recursive: true });
|
|
1576
|
-
createdDirectory = true;
|
|
1577
|
-
}
|
|
1578
|
-
const files = [];
|
|
1579
|
-
for (const [relativePath, content] of Object.entries(scaffoldFiles)) {
|
|
1580
|
-
const absolutePath = path.join(targetPath, relativePath);
|
|
1581
|
-
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
1582
|
-
if (await pathExists(absolutePath)) {
|
|
1583
|
-
const existingContent = await fs.readFile(absolutePath, "utf8");
|
|
1584
|
-
if (existingContent !== content) {
|
|
1585
|
-
throw new PmCliError(`Scaffold file "${relativePath}" already exists with different content in "${targetPath}". Choose a new target path or remove conflicting files.`, EXIT_CODE.CONFLICT);
|
|
1586
|
-
}
|
|
1587
|
-
files.push({
|
|
1588
|
-
path: relativePath,
|
|
1589
|
-
status: "unchanged",
|
|
1590
|
-
});
|
|
1591
|
-
continue;
|
|
1592
|
-
}
|
|
1593
|
-
await fs.writeFile(absolutePath, content, "utf8");
|
|
1594
|
-
files.push({
|
|
1595
|
-
path: relativePath,
|
|
1596
|
-
status: "created",
|
|
1597
|
-
});
|
|
1598
|
-
}
|
|
1599
|
-
return {
|
|
1600
|
-
extension_name: extensionName,
|
|
1601
|
-
command_name: commandName,
|
|
1602
|
-
target_path: targetPath,
|
|
1603
|
-
created_directory: createdDirectory,
|
|
1604
|
-
files,
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
function classifyDoctorLoadFailureWarnings(loadFailures) {
|
|
1608
|
-
const warnings = [];
|
|
1609
|
-
for (const failure of loadFailures) {
|
|
1610
|
-
const normalizedError = failure.error.toLowerCase();
|
|
1611
|
-
if (normalizedError.includes("cannot find package '@unbrained/pm-cli'") ||
|
|
1612
|
-
normalizedError.includes('cannot find module "@unbrained/pm-cli"') ||
|
|
1613
|
-
normalizedError.includes("cannot find module '@unbrained/pm-cli'")) {
|
|
1614
|
-
warnings.push(`extension_load_failed_sdk_dependency_missing:${failure.name}`);
|
|
1615
|
-
}
|
|
1616
|
-
if (normalizedError.includes("cannot use import statement outside a module") ||
|
|
1617
|
-
normalizedError.includes("to load an es module") ||
|
|
1618
|
-
normalizedError.includes("must use import to load es module")) {
|
|
1619
|
-
warnings.push(`extension_load_failed_module_mode_mismatch:${failure.name}`);
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
return [...new Set(warnings)].sort((left, right) => left.localeCompare(right));
|
|
1623
|
-
}
|
|
1624
|
-
function buildExtensionTriageSummary(scope, warnings, extensions, options = {}) {
|
|
1625
|
-
const normalizedWarnings = [...new Set(warnings)].sort((left, right) => left.localeCompare(right));
|
|
1626
|
-
const managedTotal = extensions.filter((entry) => entry.managed).length;
|
|
1627
|
-
const enabledTotal = extensions.filter((entry) => entry.enabled).length;
|
|
1628
|
-
const activeTotal = extensions.filter((entry) => entry.active).length;
|
|
1629
|
-
const updateAvailableTotal = extensions.filter((entry) => entry.update_available === true).length;
|
|
1630
|
-
const unmanagedExtensions = extensions.filter((entry) => entry.managed === false);
|
|
1631
|
-
const unmanagedExpectedExtensions = unmanagedExtensions
|
|
1632
|
-
.filter((entry) => isExpectedUnmanagedExtension(entry))
|
|
1633
|
-
.map((entry) => entry.name)
|
|
1634
|
-
.sort((left, right) => left.localeCompare(right));
|
|
1635
|
-
const unmanagedActionRequiredExtensions = unmanagedExtensions
|
|
1636
|
-
.filter((entry) => !isExpectedUnmanagedExtension(entry))
|
|
1637
|
-
.map((entry) => entry.name)
|
|
1638
|
-
.sort((left, right) => left.localeCompare(right));
|
|
1639
|
-
const updateCheckStatusTotals = {
|
|
1640
|
-
checked: 0,
|
|
1641
|
-
skipped_unmanaged: 0,
|
|
1642
|
-
skipped_non_github: 0,
|
|
1643
|
-
failed: 0,
|
|
1644
|
-
not_checked: 0,
|
|
1645
|
-
};
|
|
1646
|
-
for (const entry of extensions) {
|
|
1647
|
-
updateCheckStatusTotals[entry.update_check_status] += 1;
|
|
1648
|
-
}
|
|
1649
|
-
const updateCheckFailedTotal = updateCheckStatusTotals.failed;
|
|
1650
|
-
const skippedUnmanagedTotal = updateCheckStatusTotals.skipped_unmanaged;
|
|
1651
|
-
const skippedNonGithubTotal = updateCheckStatusTotals.skipped_non_github;
|
|
1652
|
-
const updateHealthPartial = unmanagedActionRequiredExtensions.length > 0;
|
|
1653
|
-
const updateHealthCoverage = updateHealthPartial ? "partial" : "full";
|
|
1654
|
-
const partialCoverageWarnings = updateHealthPartial
|
|
1655
|
-
? [`extension_update_health_partial_coverage:skipped_unmanaged:${unmanagedActionRequiredExtensions.length}`]
|
|
1656
|
-
: [];
|
|
1657
|
-
const effectiveWarnings = [...new Set([...normalizedWarnings, ...partialCoverageWarnings])].sort((left, right) => left.localeCompare(right));
|
|
1658
|
-
const warningCodes = [...new Set(effectiveWarnings.map((value) => warningCode(value)))].sort((left, right) => left.localeCompare(right));
|
|
1659
|
-
const policyWarnings = summarizePolicyWarnings(effectiveWarnings);
|
|
1660
|
-
const scopeFlag = scope === "global" ? "--global" : "--project";
|
|
1661
|
-
const remediation = [];
|
|
1662
|
-
if (normalizedWarnings.length > 0) {
|
|
1663
|
-
if (normalizedWarnings.some((warning) => warning.startsWith("extension_manifest_"))) {
|
|
1664
|
-
remediation.push(`Run ${lifecycleFlagCommand(options, "explore")} ${scopeFlag} to inspect discovered manifests and directories.`);
|
|
1665
|
-
}
|
|
1666
|
-
if (normalizedWarnings.some((warning) => warning.startsWith("extension_capability_unknown:"))) {
|
|
1667
|
-
remediation.push(`Unknown extension capabilities detected. Allowed capabilities: ${KNOWN_EXTENSION_CAPABILITIES.join(", ")}. ` +
|
|
1668
|
-
"Review extension_capability_unknown warning details for suggested replacements.");
|
|
1669
|
-
}
|
|
1670
|
-
if (normalizedWarnings.some((warning) => warning.startsWith("extension_capability_legacy_alias:"))) {
|
|
1671
|
-
remediation.push("Legacy extension capability aliases were auto-remapped to canonical capabilities. " +
|
|
1672
|
-
"Update manifests to canonical names (migration/validation -> schema).");
|
|
1673
|
-
}
|
|
1674
|
-
if (normalizedWarnings.some((warning) => warning.startsWith("extension_command_definition_legacy_handler_alias:"))) {
|
|
1675
|
-
remediation.push("Extension command definitions using legacy handler were auto-remapped. " +
|
|
1676
|
-
"Update command definitions to use run: (context) => ... for forward compatibility.");
|
|
1677
|
-
}
|
|
1678
|
-
if (normalizedWarnings.some((warning) => warning.startsWith("extension_load_failed_sdk_dependency_missing:"))) {
|
|
1679
|
-
remediation.push(`Detected extension load failures caused by missing SDK dependency resolution. ` +
|
|
1680
|
-
`Ensure extension package dependencies include "@unbrained/pm-cli" and reinstall dependencies before running ${lifecycleFlagCommand(options, "doctor")} ${scopeFlag}.`);
|
|
1681
|
-
}
|
|
1682
|
-
if (normalizedWarnings.some((warning) => warning.startsWith("extension_load_failed_module_mode_mismatch:"))) {
|
|
1683
|
-
remediation.push(`Detected extension module-mode mismatches. For ESM-based extension entries/imports, set package.json "type": "module" ` +
|
|
1684
|
-
`or use an explicit .mjs entry and rerun ${lifecycleFlagCommand(options, "doctor")} ${scopeFlag}.`);
|
|
1685
|
-
}
|
|
1686
|
-
if (updateCheckFailedTotal > 0) {
|
|
1687
|
-
remediation.push(`Run ${lifecycleFlagCommand(options, "manage")} ${scopeFlag} after validating network and repository access.`);
|
|
1688
|
-
}
|
|
1689
|
-
if (normalizedWarnings.some((warning) => warning.startsWith("extension_manager_state_"))) {
|
|
1690
|
-
remediation.push(`Review and repair ${scope} managed extension state file if schema/read warnings persist.`);
|
|
1691
|
-
}
|
|
1692
|
-
if (policyWarnings.warning_count > 0) {
|
|
1693
|
-
remediation.push("Extension governance policy warnings detected. Review settings.extensions.policy mode and allow/block lists to confirm intended capabilities and registration surfaces.");
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
if (updateHealthPartial) {
|
|
1697
|
-
remediation.push(`Update-check coverage is partial because unmanaged extensions need adoption. Adopt existing installs via ${lifecycleFlagCommand(options, "manage")} ${scopeFlag} --fix-managed-state (or ${lifecycleFlagCommand(options, "adopt-all")} ${scopeFlag}, ${lifecycleFlagCommand(options, "adopt")} <name> ${scopeFlag}, or reinstall via ${lifecycleFlagCommand(options, "install")} ${scopeFlag} <source>).`);
|
|
1698
|
-
}
|
|
1699
|
-
else if (skippedUnmanagedTotal > 0) {
|
|
1700
|
-
remediation.push(`Loaded unmanaged extensions are currently treated as informational. Use ${lifecycleFlagCommand(options, "manage")} ${scopeFlag} --fix-managed-state to adopt them for update checks.`);
|
|
1701
|
-
}
|
|
1702
|
-
if (skippedNonGithubTotal > 0) {
|
|
1703
|
-
remediation.push(`Non-GitHub managed extensions are skipped by update checks. Use doctor output for non-update diagnostics.`);
|
|
1704
|
-
}
|
|
1705
|
-
if (updateAvailableTotal > 0) {
|
|
1706
|
-
remediation.push(`Update available managed extensions via ${lifecycleFlagCommand(options, "install")} ${scopeFlag} <source>.`);
|
|
1707
|
-
}
|
|
1708
|
-
if (remediation.length === 0) {
|
|
1709
|
-
remediation.push(`No immediate action required. Re-run ${lifecycleFlagCommand(options, "manage")} ${scopeFlag} after extension changes.`);
|
|
1710
|
-
}
|
|
1711
|
-
return {
|
|
1712
|
-
status: effectiveWarnings.length === 0 ? "ok" : "warn",
|
|
1713
|
-
warning_count: effectiveWarnings.length,
|
|
1714
|
-
warning_codes: warningCodes,
|
|
1715
|
-
warnings: effectiveWarnings,
|
|
1716
|
-
policy_warning_count: policyWarnings.warning_count,
|
|
1717
|
-
policy_violation_count: policyWarnings.violation_count,
|
|
1718
|
-
policy_blocked_count: policyWarnings.blocked_count,
|
|
1719
|
-
total_extensions: extensions.length,
|
|
1720
|
-
managed_total: managedTotal,
|
|
1721
|
-
enabled_total: enabledTotal,
|
|
1722
|
-
active_total: activeTotal,
|
|
1723
|
-
update_available_total: updateAvailableTotal,
|
|
1724
|
-
update_health_coverage: updateHealthCoverage,
|
|
1725
|
-
update_health_partial: updateHealthPartial,
|
|
1726
|
-
unmanaged_loaded_extension_count: unmanagedExtensions.length,
|
|
1727
|
-
unmanaged_loaded_extensions: unmanagedExtensions
|
|
1728
|
-
.map((entry) => entry.name)
|
|
1729
|
-
.sort((left, right) => left.localeCompare(right)),
|
|
1730
|
-
unmanaged_expected_extension_count: unmanagedExpectedExtensions.length,
|
|
1731
|
-
unmanaged_expected_extensions: unmanagedExpectedExtensions,
|
|
1732
|
-
unmanaged_action_required_extension_count: unmanagedActionRequiredExtensions.length,
|
|
1733
|
-
unmanaged_action_required_extensions: unmanagedActionRequiredExtensions,
|
|
1734
|
-
update_check_status_totals: updateCheckStatusTotals,
|
|
1735
|
-
update_check_failed_total: updateCheckFailedTotal,
|
|
1736
|
-
top_warnings: effectiveWarnings.slice(0, 8),
|
|
1737
|
-
remediation,
|
|
1738
|
-
};
|
|
1739
|
-
}
|
|
1740
|
-
function parseDoctorDetailMode(raw) {
|
|
1741
|
-
if (!raw || raw.trim().length === 0) {
|
|
1742
|
-
return "summary";
|
|
1743
|
-
}
|
|
1744
|
-
const normalized = raw.trim().toLowerCase();
|
|
1745
|
-
if (normalized === "summary" || normalized === "deep") {
|
|
1746
|
-
return normalized;
|
|
1747
|
-
}
|
|
1748
|
-
throw new PmCliError(`Invalid --detail value "${raw}". Expected summary or deep.`, EXIT_CODE.USAGE);
|
|
1749
|
-
}
|
|
1750
|
-
function warningCode(value) {
|
|
1751
|
-
const normalized = value.trim();
|
|
1752
|
-
const separator = normalized.indexOf(":");
|
|
1753
|
-
if (separator === -1) {
|
|
1754
|
-
return normalized;
|
|
1755
|
-
}
|
|
1756
|
-
return normalized.slice(0, separator);
|
|
1757
|
-
}
|
|
1758
|
-
function collectUnknownCapabilityGuidance(warnings) {
|
|
1759
|
-
const seen = new Set();
|
|
1760
|
-
const guidance = [];
|
|
1761
|
-
for (const warning of warnings) {
|
|
1762
|
-
const parsedDetails = (() => {
|
|
1763
|
-
const unknownWarning = parseUnknownExtensionCapabilityWarning(warning);
|
|
1764
|
-
if (unknownWarning) {
|
|
1765
|
-
return [unknownWarning];
|
|
1766
|
-
}
|
|
1767
|
-
return parseLegacyExtensionCapabilityAliasWarning(warning);
|
|
1768
|
-
})();
|
|
1769
|
-
for (const parsed of parsedDetails) {
|
|
1770
|
-
const key = `${parsed.layer}:${parsed.name}:${parsed.capability}`;
|
|
1771
|
-
if (seen.has(key)) {
|
|
1772
|
-
continue;
|
|
1773
|
-
}
|
|
1774
|
-
seen.add(key);
|
|
1775
|
-
guidance.push(parsed);
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
return guidance;
|
|
1779
|
-
}
|
|
1780
|
-
function buildCapabilityContractMetadata() {
|
|
1781
|
-
return {
|
|
1782
|
-
version: EXTENSION_CAPABILITY_CONTRACT.version,
|
|
1783
|
-
capabilities: [...EXTENSION_CAPABILITY_CONTRACT.capabilities],
|
|
1784
|
-
legacy_aliases: { ...EXTENSION_CAPABILITY_CONTRACT.legacy_aliases },
|
|
1785
|
-
};
|
|
1786
|
-
}
|
|
1787
|
-
function isExpectedUnmanagedExtension(entry) {
|
|
1788
|
-
const normalizedName = normalizeExtensionNameForMatch(entry.name);
|
|
1789
|
-
const normalizedDirectory = normalizeExtensionNameForMatch(entry.directory);
|
|
1790
|
-
if (normalizedName.startsWith("builtin-")) {
|
|
1791
|
-
return true;
|
|
1792
|
-
}
|
|
1793
|
-
return normalizedDirectory === "beads" || normalizedDirectory === "todos";
|
|
1794
|
-
}
|
|
1795
|
-
function buildDoctorConsistencySummary(scope, installedExtensions, loadedExtensions, failedLoads, disabledByFlag) {
|
|
1796
|
-
if (scope !== "project" || disabledByFlag) {
|
|
1797
|
-
return {
|
|
1798
|
-
warnings: [],
|
|
1799
|
-
summary: {
|
|
1800
|
-
active_project_count: 0,
|
|
1801
|
-
loaded_project_count: 0,
|
|
1802
|
-
active_project_names: [],
|
|
1803
|
-
loaded_project_names: [],
|
|
1804
|
-
missing_active_project_names: [],
|
|
1805
|
-
},
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
const activeProjectNames = [
|
|
1809
|
-
...new Set(installedExtensions
|
|
1810
|
-
.filter((entry) => entry.active)
|
|
1811
|
-
.map((entry) => normalizeExtensionNameForMatch(entry.name))),
|
|
1812
|
-
].sort((left, right) => left.localeCompare(right));
|
|
1813
|
-
const loadedProjectNames = [
|
|
1814
|
-
...new Set(loadedExtensions
|
|
1815
|
-
.filter((entry) => entry.layer === "project")
|
|
1816
|
-
.map((entry) => normalizeExtensionNameForMatch(entry.name))),
|
|
1817
|
-
].sort((left, right) => left.localeCompare(right));
|
|
1818
|
-
const failedLoadNames = new Set(failedLoads.map((entry) => normalizeExtensionNameForMatch(entry.name)));
|
|
1819
|
-
const missingActiveProjectNames = activeProjectNames
|
|
1820
|
-
.filter((name) => !loadedProjectNames.includes(name) && !failedLoadNames.has(name))
|
|
1821
|
-
.sort((left, right) => left.localeCompare(right));
|
|
1822
|
-
const warnings = missingActiveProjectNames.length > 0
|
|
1823
|
-
? [`extension_doctor_consistency_active_not_loaded:${missingActiveProjectNames.join(",")}`]
|
|
1824
|
-
: [];
|
|
1825
|
-
return {
|
|
1826
|
-
warnings,
|
|
1827
|
-
summary: {
|
|
1828
|
-
active_project_count: activeProjectNames.length,
|
|
1829
|
-
loaded_project_count: loadedProjectNames.length,
|
|
1830
|
-
active_project_names: activeProjectNames,
|
|
1831
|
-
loaded_project_names: loadedProjectNames,
|
|
1832
|
-
missing_active_project_names: missingActiveProjectNames,
|
|
1833
|
-
},
|
|
1834
|
-
};
|
|
1835
|
-
}
|
|
1836
558
|
export async function runExtension(target, options, global) {
|
|
1837
559
|
const action = resolveAction(target, options);
|
|
1838
560
|
if ((options.strictExit === true || options.failOnWarn === true) && action !== "doctor") {
|
|
@@ -2012,89 +734,88 @@ export async function runExtension(target, options, global) {
|
|
|
2012
734
|
ref: options.ref,
|
|
2013
735
|
});
|
|
2014
736
|
const resolvedSource = await resolveInstallSource(installSource);
|
|
2015
|
-
const settings = await readSettings(resolvedRoots.settings_root);
|
|
2016
|
-
const managedStateRead = await readManagedExtensionState(resolvedRoots.selected_root);
|
|
2017
|
-
warnings.push(...managedStateRead.warnings);
|
|
2018
737
|
try {
|
|
2019
738
|
const validated = await validateExtensionDirectory(resolvedSource.directory);
|
|
2020
739
|
const destinationDirectoryName = normalizeManagedDirectoryName(validated.manifest.name);
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
const sourceRecord = bundledAliasName
|
|
2032
|
-
? {
|
|
2033
|
-
kind: "builtin",
|
|
2034
|
-
input: bundledAliasName,
|
|
2035
|
-
location: bundledAliasName,
|
|
2036
|
-
name: bundledAliasName,
|
|
740
|
+
return await withExtensionInstallLock(resolvedRoots.settings_root, destinationDirectoryName, async () => {
|
|
741
|
+
const settings = await readSettings(resolvedRoots.settings_root);
|
|
742
|
+
const managedStateRead = await readManagedExtensionState(resolvedRoots.selected_root);
|
|
743
|
+
warnings.push(...managedStateRead.warnings);
|
|
744
|
+
const destinationDirectory = path.join(resolvedRoots.selected_root, destinationDirectoryName);
|
|
745
|
+
const destinationExists = await pathExists(destinationDirectory);
|
|
746
|
+
const installInPlace = await areDirectoriesEquivalent(validated.directory, destinationDirectory);
|
|
747
|
+
await fs.mkdir(resolvedRoots.selected_root, { recursive: true });
|
|
748
|
+
if (!installInPlace) {
|
|
749
|
+
await copyExtensionDirectoryForInstall(validated.directory, destinationDirectory);
|
|
2037
750
|
}
|
|
2038
|
-
|
|
751
|
+
const sourceRecord = bundledAliasName
|
|
2039
752
|
? {
|
|
2040
|
-
kind: "
|
|
2041
|
-
input:
|
|
2042
|
-
location:
|
|
753
|
+
kind: "builtin",
|
|
754
|
+
input: bundledAliasName,
|
|
755
|
+
location: bundledAliasName,
|
|
756
|
+
name: bundledAliasName,
|
|
2043
757
|
}
|
|
2044
|
-
: installSource.kind === "
|
|
758
|
+
: installSource.kind === "local"
|
|
2045
759
|
? {
|
|
2046
|
-
kind: "
|
|
760
|
+
kind: "local",
|
|
2047
761
|
input: installSource.input,
|
|
2048
|
-
location:
|
|
2049
|
-
package: resolvedSource.npm_package,
|
|
2050
|
-
version: resolvedSource.npm_version,
|
|
762
|
+
location: installSource.absolute_path,
|
|
2051
763
|
}
|
|
2052
|
-
:
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
source: sourceRecord,
|
|
2078
|
-
});
|
|
2079
|
-
await writeManagedExtensionState(resolvedRoots.selected_root, managedState);
|
|
2080
|
-
const activationChanged = ensureActivated(settings, validated.manifest.name);
|
|
2081
|
-
if (activationChanged) {
|
|
2082
|
-
await writeSettings(resolvedRoots.settings_root, settings, "settings:write");
|
|
2083
|
-
}
|
|
2084
|
-
return withResult({
|
|
2085
|
-
extension: {
|
|
764
|
+
: installSource.kind === "npm"
|
|
765
|
+
? {
|
|
766
|
+
kind: "npm",
|
|
767
|
+
input: installSource.input,
|
|
768
|
+
location: resolvedSource.resolved_subpath ?? ".",
|
|
769
|
+
package: resolvedSource.npm_package,
|
|
770
|
+
version: resolvedSource.npm_version,
|
|
771
|
+
}
|
|
772
|
+
: {
|
|
773
|
+
kind: "github",
|
|
774
|
+
input: installSource.input,
|
|
775
|
+
location: resolvedSource.resolved_subpath ?? installSource.subpath ?? ".",
|
|
776
|
+
repository: installSource.repository,
|
|
777
|
+
owner: installSource.owner,
|
|
778
|
+
repo: installSource.repo,
|
|
779
|
+
ref: installSource.ref,
|
|
780
|
+
subpath: resolvedSource.resolved_subpath ?? installSource.subpath,
|
|
781
|
+
commit: resolvedSource.commit,
|
|
782
|
+
};
|
|
783
|
+
const now = nowIso();
|
|
784
|
+
const existingManagedEntry = managedStateRead.state.entries.find((entry) => normalizeExtensionNameForMatch(entry.name) === normalizeExtensionNameForMatch(validated.manifest.name));
|
|
785
|
+
const sourceUnchanged = existingManagedEntry !== undefined &&
|
|
786
|
+
existingManagedEntry.manifest_version === validated.manifest.version &&
|
|
787
|
+
managedExtensionSourcesEquivalent(existingManagedEntry.source, sourceRecord);
|
|
788
|
+
const managedState = upsertManagedEntry(managedStateRead.state, {
|
|
2086
789
|
name: validated.manifest.name,
|
|
2087
|
-
version: validated.manifest.version,
|
|
2088
|
-
entry: validated.manifest.entry,
|
|
2089
|
-
capabilities: validated.manifest.capabilities,
|
|
2090
790
|
directory: destinationDirectoryName,
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
791
|
+
scope,
|
|
792
|
+
manifest_version: validated.manifest.version,
|
|
793
|
+
manifest_entry: validated.manifest.entry,
|
|
794
|
+
capabilities: [...validated.manifest.capabilities],
|
|
795
|
+
installed_at: existingManagedEntry?.installed_at ?? now,
|
|
796
|
+
updated_at: sourceUnchanged ? existingManagedEntry.updated_at : now,
|
|
797
|
+
source: sourceRecord,
|
|
798
|
+
});
|
|
799
|
+
await writeManagedExtensionState(resolvedRoots.selected_root, managedState);
|
|
800
|
+
const activationChanged = ensureActivated(settings, validated.manifest.name);
|
|
801
|
+
if (activationChanged) {
|
|
802
|
+
await writeSettings(resolvedRoots.settings_root, settings, "settings:write");
|
|
803
|
+
}
|
|
804
|
+
return withResult({
|
|
805
|
+
extension: {
|
|
806
|
+
name: validated.manifest.name,
|
|
807
|
+
version: validated.manifest.version,
|
|
808
|
+
entry: validated.manifest.entry,
|
|
809
|
+
capabilities: validated.manifest.capabilities,
|
|
810
|
+
directory: destinationDirectoryName,
|
|
811
|
+
},
|
|
812
|
+
source: sourceRecord,
|
|
813
|
+
destination_path: destinationDirectory,
|
|
814
|
+
overwritten: destinationExists && !installInPlace,
|
|
815
|
+
installed_in_place: installInPlace,
|
|
816
|
+
activated: true,
|
|
817
|
+
settings_changed: activationChanged,
|
|
818
|
+
});
|
|
2098
819
|
});
|
|
2099
820
|
}
|
|
2100
821
|
finally {
|
|
@@ -2612,4 +1333,4 @@ export async function runExtension(target, options, global) {
|
|
|
2612
1333
|
throw new PmCliError(`Unsupported extension action "${action}".`, EXIT_CODE.USAGE);
|
|
2613
1334
|
}
|
|
2614
1335
|
//# sourceMappingURL=extension.js.map
|
|
2615
|
-
//# debugId=
|
|
1336
|
+
//# debugId=bec21924-aaa9-50f0-83ba-af92671813ea
|