@topogram/cli 0.3.63 → 0.3.64
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/package.json +1 -1
- package/src/adoption/plan.d.ts +6 -0
- package/src/adoption/reporting.d.ts +10 -0
- package/src/adoption/review-groups.d.ts +6 -0
- package/src/agent-brief.d.ts +3 -0
- package/src/agent-brief.js +495 -0
- package/src/agent-ops/query-builders.d.ts +26 -0
- package/src/archive/archive.d.ts +2 -0
- package/src/archive/compact.d.ts +1 -0
- package/src/archive/unarchive.d.ts +1 -0
- package/src/catalog.d.ts +10 -0
- package/src/catalog.js +62 -66
- package/src/cli/catalog-alias.d.ts +1 -0
- package/src/cli/command-parser.js +38 -0
- package/src/cli/command-parsers/core.js +102 -0
- package/src/cli/command-parsers/generator.js +39 -0
- package/src/cli/command-parsers/import.js +44 -0
- package/src/cli/command-parsers/legacy-workflow.js +21 -0
- package/src/cli/command-parsers/project.js +47 -0
- package/src/cli/command-parsers/sdlc.js +47 -0
- package/src/cli/command-parsers/shared.js +51 -0
- package/src/cli/command-parsers/template.js +48 -0
- package/src/cli/commands/agent.js +47 -0
- package/src/cli/commands/catalog.js +617 -0
- package/src/cli/commands/check.js +268 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/emit.js +149 -0
- package/src/cli/commands/generate.js +96 -0
- package/src/cli/commands/generator-policy.js +785 -0
- package/src/cli/commands/generator.js +443 -0
- package/src/cli/commands/import-runner.js +157 -0
- package/src/cli/commands/import.js +1734 -0
- package/src/cli/commands/inspect.js +55 -0
- package/src/cli/commands/new.js +94 -0
- package/src/cli/commands/package.js +815 -0
- package/src/cli/commands/query.js +1302 -0
- package/src/cli/commands/release-rollout.js +257 -0
- package/src/cli/commands/release-shared.js +528 -0
- package/src/cli/commands/release-status.js +429 -0
- package/src/cli/commands/release.js +107 -0
- package/src/cli/commands/sdlc.js +168 -0
- package/src/cli/commands/setup.js +76 -0
- package/src/cli/commands/source.js +291 -0
- package/src/cli/commands/template-runner.js +198 -0
- package/src/cli/commands/template.js +2145 -0
- package/src/cli/commands/trust.js +219 -0
- package/src/cli/commands/version.js +40 -0
- package/src/cli/commands/widget.js +168 -0
- package/src/cli/commands/workflow.js +63 -0
- package/src/cli/dispatcher.js +392 -0
- package/src/cli/help-dispatch.js +188 -0
- package/src/cli/help.js +296 -0
- package/src/cli/migration-guidance.js +59 -0
- package/src/cli/options.js +96 -0
- package/src/cli/output-safety.js +107 -0
- package/src/cli/path-normalization.js +29 -0
- package/src/cli.js +47 -11711
- package/src/example-implementation.d.ts +2 -0
- package/src/format.d.ts +1 -0
- package/src/generator/check.d.ts +1 -0
- package/src/generator/context/bundle.d.ts +1 -0
- package/src/generator/context/shared.d.ts +2 -0
- package/src/generator/native/parity-bundle.js +2 -1
- package/src/generator/surfaces/web/html-escape.js +22 -0
- package/src/generator/surfaces/web/react.js +10 -8
- package/src/generator/surfaces/web/sveltekit.js +7 -5
- package/src/generator/surfaces/web/vanilla.js +8 -4
- package/src/generator.d.ts +2 -0
- package/src/github-client.js +520 -0
- package/src/import/core/shared.js +20 -62
- package/src/import/extractors/api/flutter-dio.js +4 -8
- package/src/import/extractors/api/react-native-repository.js +4 -8
- package/src/import/index.d.ts +4 -0
- package/src/import/provenance.d.ts +4 -0
- package/src/new-project.js +100 -11
- package/src/npm-safety.js +79 -0
- package/src/parser.d.ts +1 -0
- package/src/path-helpers.d.ts +1 -0
- package/src/path-helpers.js +20 -0
- package/src/project-config.js +1 -0
- package/src/reconcile/docs.d.ts +8 -0
- package/src/reconcile/journeys.d.ts +1 -0
- package/src/resolver.d.ts +1 -0
- package/src/runtime-support.js +29 -0
- package/src/sdlc/adopt.d.ts +1 -0
- package/src/sdlc/check.d.ts +1 -0
- package/src/sdlc/explain.d.ts +1 -0
- package/src/sdlc/release.d.ts +1 -0
- package/src/sdlc/scaffold.d.ts +1 -0
- package/src/sdlc/transition.d.ts +1 -0
- package/src/text-helpers.d.ts +6 -0
- package/src/text-helpers.js +245 -0
- package/src/topogram-config.js +306 -0
- package/src/validator.d.ts +2 -0
- package/src/workflows/adoption/index.js +26 -0
- package/src/workflows/docs-generate.js +262 -0
- package/src/workflows/docs-scan.js +703 -0
- package/src/workflows/docs.js +15 -0
- package/src/workflows/import-app/api.js +799 -0
- package/src/workflows/import-app/db.js +538 -0
- package/src/workflows/import-app/index.js +30 -0
- package/src/workflows/import-app/shared.js +218 -0
- package/src/workflows/import-app/ui.js +443 -0
- package/src/workflows/import-app/workflow.js +159 -0
- package/src/workflows/reconcile/adoption-plan.js +742 -0
- package/src/workflows/reconcile/auth.js +692 -0
- package/src/workflows/reconcile/bundle-core.js +600 -0
- package/src/workflows/reconcile/bundle-shared.js +75 -0
- package/src/workflows/reconcile/candidate-model.js +477 -0
- package/src/workflows/reconcile/canonical-surface.js +264 -0
- package/src/workflows/reconcile/gap-report.js +333 -0
- package/src/workflows/reconcile/ids.js +6 -0
- package/src/workflows/reconcile/impacts.js +625 -0
- package/src/workflows/reconcile/index.js +7 -0
- package/src/workflows/reconcile/renderers.js +461 -0
- package/src/workflows/reconcile/summary.js +90 -0
- package/src/workflows/reconcile/workflow.js +309 -0
- package/src/workflows/shared.js +189 -0
- package/src/workflows/types.d.ts +93 -0
- package/src/workflows.d.ts +1 -0
- package/src/workflows.js +10 -7652
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import childProcess from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { stableStringify } from "../../format.js";
|
|
8
|
+
import { assertSafeNpmSpec, localNpmrcEnv } from "../../npm-safety.js";
|
|
9
|
+
import {
|
|
10
|
+
catalogDoctorPackageDiagnostic,
|
|
11
|
+
runNpmViewPackageSpec
|
|
12
|
+
} from "./catalog.js";
|
|
13
|
+
|
|
14
|
+
export const CLI_PACKAGE_NAME = "@topogram/cli";
|
|
15
|
+
export const NPMJS_REGISTRY = "https://registry.npmjs.org";
|
|
16
|
+
|
|
17
|
+
const PACKAGE_UPDATE_CLI_CHECK_SCRIPTS = [
|
|
18
|
+
"cli:surface",
|
|
19
|
+
"doctor",
|
|
20
|
+
"catalog:show",
|
|
21
|
+
"catalog:template-show",
|
|
22
|
+
"check",
|
|
23
|
+
"pack:check",
|
|
24
|
+
"verify"
|
|
25
|
+
];
|
|
26
|
+
const PACKAGE_UPDATE_CLI_INFO_SCRIPTS = ["cli:surface", "doctor", "catalog:show", "catalog:template-show"];
|
|
27
|
+
const PACKAGE_UPDATE_CLI_VERIFICATION_SCRIPTS = ["verify", "pack:check", "check"];
|
|
28
|
+
const ENGINE_ROOT = decodeURIComponent(new URL("../../../", import.meta.url).pathname);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
33
|
+
export function printPackageHelp() {
|
|
34
|
+
console.log("Usage: topogram package update-cli <version|--latest> [--json]");
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log("Updates a consumer project to a Topogram CLI version and runs verification when dependencies are current.");
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log("Behavior:");
|
|
39
|
+
console.log(" - npmjs package inspection confirms the requested public CLI version.");
|
|
40
|
+
console.log(" - npm install updates package.json and package-lock.json.");
|
|
41
|
+
console.log(" - Available consumer verification scripts run after install.");
|
|
42
|
+
console.log(` - Recognized scripts: ${PACKAGE_UPDATE_CLI_CHECK_SCRIPTS.join(", ")}.`);
|
|
43
|
+
console.log(" - Verification scripts are selected by strength: verify, then pack:check, then check.");
|
|
44
|
+
console.log("");
|
|
45
|
+
console.log("Examples:");
|
|
46
|
+
console.log(" topogram package update-cli 0.3.5");
|
|
47
|
+
console.log(" topogram package update-cli --latest");
|
|
48
|
+
console.log(" topogram package update-cli --latest --json");
|
|
49
|
+
console.log("");
|
|
50
|
+
console.log("Auth help:");
|
|
51
|
+
console.log(" topogram setup package-auth");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {unknown} error
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function messageFromError(error) {
|
|
59
|
+
return error instanceof Error ? error.message : String(error);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} requested
|
|
64
|
+
* @param {{ cwd?: string }} [options]
|
|
65
|
+
* @returns {{ ok: boolean, packageName: string, requestedVersion: string, requestedLatest: boolean, dependencySpec: string, checkedVersion: string, packageCheckSource: "npm", dependencyUpdatedBy: "npm-install"|"manifest-lockfile"|"version-convention", lockfileSanitized: boolean, versionConventionUpdated: boolean, versionConventionPath: string|null, scriptsRun: string[], skippedScripts: string[], diagnostics: Array<Record<string, any>>, errors: string[] }}
|
|
66
|
+
*/
|
|
67
|
+
export function buildPackageUpdateCliPayload(requested, options = {}) {
|
|
68
|
+
const cwd = options.cwd || process.cwd();
|
|
69
|
+
const requestedLatest = requested === "latest" || requested === "--latest";
|
|
70
|
+
/** @type {Array<Record<string, any>>} */
|
|
71
|
+
const diagnostics = [];
|
|
72
|
+
const version = requestedLatest
|
|
73
|
+
? resolveLatestTopogramCliVersionForPackageUpdate(cwd, diagnostics)
|
|
74
|
+
: requested;
|
|
75
|
+
if (!isPackageVersion(version)) {
|
|
76
|
+
throw new Error("topogram package update-cli requires <version> or --latest.");
|
|
77
|
+
}
|
|
78
|
+
const exactSpec = `${CLI_PACKAGE_NAME}@${version}`;
|
|
79
|
+
const dependencySpec = `${CLI_PACKAGE_NAME}@^${version}`;
|
|
80
|
+
assertSafeNpmSpec(exactSpec);
|
|
81
|
+
assertSafeNpmSpec(dependencySpec);
|
|
82
|
+
const view = runNpmForPackageUpdate(["view", `--registry=${NPMJS_REGISTRY}`, "--", exactSpec, "version"], cwd);
|
|
83
|
+
let checkedVersion = null;
|
|
84
|
+
const packageCheckSource = "npm";
|
|
85
|
+
if (view.status !== 0) {
|
|
86
|
+
throw new Error(formatPackageUpdateNpmError(exactSpec, "inspect", view));
|
|
87
|
+
} else {
|
|
88
|
+
checkedVersion = String(view.stdout || "").trim().replace(/^"|"$/g, "");
|
|
89
|
+
if (checkedVersion !== version) {
|
|
90
|
+
throw new Error(`Expected ${exactSpec}, but npm returned version '${checkedVersion || "(empty)"}'.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const lockfileSanitized = sanitizeTopogramLockForPackageUpdate(cwd, version);
|
|
94
|
+
const dependencyUpdatedBy = "npm-install";
|
|
95
|
+
const install = runNpmForPackageUpdate(["install", "--save-dev", `--registry=${NPMJS_REGISTRY}`, "--", dependencySpec], cwd);
|
|
96
|
+
if (install.status !== 0) {
|
|
97
|
+
throw new Error(formatPackageUpdateNpmError(dependencySpec, "install", install));
|
|
98
|
+
}
|
|
99
|
+
const versionConvention = writeTopogramCliVersionConventionIfPresent(cwd, version);
|
|
100
|
+
const packageJson = readPackageJsonForUpdate(cwd);
|
|
101
|
+
const scripts = packageJson.scripts && typeof packageJson.scripts === "object" ? packageJson.scripts : {};
|
|
102
|
+
const scriptsRun = [];
|
|
103
|
+
const skippedScripts = [];
|
|
104
|
+
const scriptsToRun = packageUpdateCliScriptsToRun(scripts);
|
|
105
|
+
for (const scriptName of PACKAGE_UPDATE_CLI_INFO_SCRIPTS) {
|
|
106
|
+
if (!Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
107
|
+
skippedScripts.push(scriptName);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const scriptName of PACKAGE_UPDATE_CLI_VERIFICATION_SCRIPTS) {
|
|
111
|
+
if (!Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
112
|
+
skippedScripts.push(scriptName);
|
|
113
|
+
} else if (!scriptsToRun.includes(scriptName)) {
|
|
114
|
+
const coveringScript = scriptsToRun.find((candidate) =>
|
|
115
|
+
PACKAGE_UPDATE_CLI_VERIFICATION_SCRIPTS.includes(candidate)
|
|
116
|
+
);
|
|
117
|
+
skippedScripts.push(`${scriptName} (covered by ${coveringScript})`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const scriptName of scriptsToRun) {
|
|
121
|
+
const result = runNpmForPackageUpdate(["run", scriptName], cwd);
|
|
122
|
+
if (result.status !== 0) {
|
|
123
|
+
throw new Error(formatPackageUpdateNpmError(`npm run ${scriptName}`, "check", result));
|
|
124
|
+
}
|
|
125
|
+
scriptsRun.push(scriptName);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
packageName: CLI_PACKAGE_NAME,
|
|
130
|
+
requestedVersion: version,
|
|
131
|
+
requestedLatest,
|
|
132
|
+
dependencySpec,
|
|
133
|
+
checkedVersion,
|
|
134
|
+
packageCheckSource,
|
|
135
|
+
dependencyUpdatedBy,
|
|
136
|
+
lockfileSanitized,
|
|
137
|
+
versionConventionUpdated: versionConvention.updated,
|
|
138
|
+
versionConventionPath: versionConvention.path,
|
|
139
|
+
scriptsRun,
|
|
140
|
+
skippedScripts,
|
|
141
|
+
diagnostics,
|
|
142
|
+
errors: []
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {Record<string, any>} scripts
|
|
148
|
+
* @returns {string[]}
|
|
149
|
+
*/
|
|
150
|
+
function packageUpdateCliScriptsToRun(scripts) {
|
|
151
|
+
const selected = [];
|
|
152
|
+
for (const scriptName of PACKAGE_UPDATE_CLI_INFO_SCRIPTS) {
|
|
153
|
+
if (Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
154
|
+
selected.push(scriptName);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const verificationScript = PACKAGE_UPDATE_CLI_VERIFICATION_SCRIPTS.find((scriptName) =>
|
|
158
|
+
Object.prototype.hasOwnProperty.call(scripts, scriptName)
|
|
159
|
+
);
|
|
160
|
+
if (verificationScript) {
|
|
161
|
+
selected.push(verificationScript);
|
|
162
|
+
}
|
|
163
|
+
return selected;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {ReturnType<typeof buildPackageUpdateCliPayload>} payload
|
|
168
|
+
* @returns {void}
|
|
169
|
+
*/
|
|
170
|
+
export function printPackageUpdateCli(payload) {
|
|
171
|
+
for (const diagnostic of payload.diagnostics) {
|
|
172
|
+
if (diagnostic.severity === "warning") {
|
|
173
|
+
console.warn(`Warning: ${diagnostic.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
console.log(`Updated ${payload.packageName} to ^${payload.requestedVersion}.`);
|
|
177
|
+
if (payload.requestedLatest) {
|
|
178
|
+
console.log(`Resolved latest version: ${payload.requestedVersion}`);
|
|
179
|
+
}
|
|
180
|
+
console.log(`Checked package: ${payload.packageName}@${payload.checkedVersion} via ${payload.packageCheckSource}`);
|
|
181
|
+
console.log(`Updated dependency: ${payload.dependencySpec} via ${payload.dependencyUpdatedBy}`);
|
|
182
|
+
if (payload.lockfileSanitized) {
|
|
183
|
+
console.log("Lockfile: refreshed existing @topogram/cli entry from registry metadata");
|
|
184
|
+
}
|
|
185
|
+
if (payload.versionConventionUpdated) {
|
|
186
|
+
console.log(`Version convention: updated ${payload.versionConventionPath}`);
|
|
187
|
+
}
|
|
188
|
+
console.log(`Checks run: ${payload.scriptsRun.join(", ") || "none"}`);
|
|
189
|
+
if (payload.skippedScripts.length > 0) {
|
|
190
|
+
console.log(`Checks skipped: ${payload.skippedScripts.join(", ")}`);
|
|
191
|
+
}
|
|
192
|
+
console.log("");
|
|
193
|
+
console.log("Next steps:");
|
|
194
|
+
console.log(" git diff package.json package-lock.json");
|
|
195
|
+
console.log(` git commit -am "Update Topogram CLI to ${payload.requestedVersion}"`);
|
|
196
|
+
console.log(" git push");
|
|
197
|
+
console.log(" confirm the repo verification workflow passes");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {{
|
|
202
|
+
* commandArgs: Record<string, any>,
|
|
203
|
+
* inputPath: string|null|undefined,
|
|
204
|
+
* json: boolean
|
|
205
|
+
* }} context
|
|
206
|
+
* @returns {number}
|
|
207
|
+
*/
|
|
208
|
+
export function runPackageCommand(context) {
|
|
209
|
+
const { commandArgs, inputPath, json } = context;
|
|
210
|
+
if (commandArgs.packageCommand !== "update-cli") {
|
|
211
|
+
throw new Error(`Unknown package command '${commandArgs.packageCommand}'`);
|
|
212
|
+
}
|
|
213
|
+
if (!inputPath) {
|
|
214
|
+
console.error("Missing required <version>.");
|
|
215
|
+
printPackageHelp();
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
const payload = buildPackageUpdateCliPayload(inputPath);
|
|
219
|
+
if (json) {
|
|
220
|
+
console.log(stableStringify(payload));
|
|
221
|
+
} else {
|
|
222
|
+
printPackageUpdateCli(payload);
|
|
223
|
+
}
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {string} cwd
|
|
229
|
+
* @returns {string|null}
|
|
230
|
+
*/
|
|
231
|
+
export function readProjectCliDependencySpec(cwd) {
|
|
232
|
+
const packagePath = path.join(cwd, "package.json");
|
|
233
|
+
if (!fs.existsSync(packagePath)) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
238
|
+
const dependencies = {
|
|
239
|
+
...(pkg.dependencies && typeof pkg.dependencies === "object" ? pkg.dependencies : {}),
|
|
240
|
+
...(pkg.devDependencies && typeof pkg.devDependencies === "object" ? pkg.devDependencies : {})
|
|
241
|
+
};
|
|
242
|
+
const spec = dependencies[CLI_PACKAGE_NAME];
|
|
243
|
+
return typeof spec === "string" && spec ? spec : null;
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @param {string|null} spec
|
|
251
|
+
* @returns {boolean}
|
|
252
|
+
*/
|
|
253
|
+
export function isLocalCliDependencySpec(spec) {
|
|
254
|
+
if (!spec) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
return spec.startsWith("file:") ||
|
|
258
|
+
spec.startsWith(".") ||
|
|
259
|
+
spec.startsWith("/") ||
|
|
260
|
+
spec.endsWith(".tgz");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @returns {{ version: string, minimum: string, ok: boolean, diagnostics: any[] }}
|
|
265
|
+
*/
|
|
266
|
+
export function checkDoctorNode() {
|
|
267
|
+
const version = process.version;
|
|
268
|
+
const minimum = "20.0.0";
|
|
269
|
+
const ok = compareSemver(version.replace(/^v/, ""), minimum) >= 0;
|
|
270
|
+
return {
|
|
271
|
+
version,
|
|
272
|
+
minimum: `>=${minimum}`,
|
|
273
|
+
ok,
|
|
274
|
+
diagnostics: ok ? [] : [{
|
|
275
|
+
code: "node_version_unsupported",
|
|
276
|
+
severity: "error",
|
|
277
|
+
message: `Topogram requires Node.js >=${minimum}; current version is ${version}.`,
|
|
278
|
+
path: null,
|
|
279
|
+
suggestedFix: "Install Node.js 20 or newer."
|
|
280
|
+
}]
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @returns {{ available: boolean, version: string|null, diagnostics: any[] }}
|
|
286
|
+
*/
|
|
287
|
+
export function checkDoctorNpm() {
|
|
288
|
+
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
289
|
+
const result = childProcess.spawnSync(npmBin, ["--version"], {
|
|
290
|
+
encoding: "utf8",
|
|
291
|
+
env: {
|
|
292
|
+
...process.env,
|
|
293
|
+
PATH: process.env.PATH || ""
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
if (result.status === 0) {
|
|
297
|
+
return {
|
|
298
|
+
available: true,
|
|
299
|
+
version: String(result.stdout || "").trim() || null,
|
|
300
|
+
diagnostics: []
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
available: false,
|
|
305
|
+
version: null,
|
|
306
|
+
diagnostics: [{
|
|
307
|
+
code: "npm_not_found",
|
|
308
|
+
severity: "error",
|
|
309
|
+
message: "npm was not found on PATH.",
|
|
310
|
+
path: null,
|
|
311
|
+
suggestedFix: "Install Node.js/npm, then rerun `topogram doctor`."
|
|
312
|
+
}]
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @returns {string}
|
|
318
|
+
*/
|
|
319
|
+
export function readInstalledCliPackageVersion() {
|
|
320
|
+
const packagePath = path.join(ENGINE_ROOT, "package.json");
|
|
321
|
+
if (!fs.existsSync(packagePath)) {
|
|
322
|
+
return "0.0.0";
|
|
323
|
+
}
|
|
324
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
325
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* @param {string} key
|
|
330
|
+
* @returns {string|null}
|
|
331
|
+
*/
|
|
332
|
+
export function npmConfigGet(key) {
|
|
333
|
+
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
334
|
+
const result = childProcess.spawnSync(npmBin, ["config", "get", key], {
|
|
335
|
+
encoding: "utf8",
|
|
336
|
+
env: {
|
|
337
|
+
...process.env,
|
|
338
|
+
PATH: process.env.PATH || ""
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
if (result.status !== 0) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const value = String(result.stdout || "").trim();
|
|
345
|
+
return value && value !== "undefined" && value !== "null" ? value : null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {string|null} value
|
|
350
|
+
* @returns {string|null}
|
|
351
|
+
*/
|
|
352
|
+
export function normalizeRegistryUrl(value) {
|
|
353
|
+
if (!value) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
return value.trim().replace(/\/+$/, "");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {string} packageSpec
|
|
361
|
+
* @returns {{ ok: boolean, checkedVersion: string|null, diagnostics: any[] }}
|
|
362
|
+
*/
|
|
363
|
+
export function checkDoctorPackageAccess(packageSpec) {
|
|
364
|
+
const result = runNpmViewPackageSpec(packageSpec);
|
|
365
|
+
if (result.status === 0) {
|
|
366
|
+
return {
|
|
367
|
+
ok: true,
|
|
368
|
+
checkedVersion: String(result.stdout || "").trim().replace(/^"|"$/g, "") || null,
|
|
369
|
+
diagnostics: []
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
checkedVersion: null,
|
|
375
|
+
diagnostics: [doctorPackageDiagnostic(packageSpec, result)]
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @param {string} spec
|
|
381
|
+
* @returns {string|null}
|
|
382
|
+
*/
|
|
383
|
+
export function registryPackageNameFromSpec(spec) {
|
|
384
|
+
if (!spec || spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("file:") || spec.endsWith(".tgz")) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
if (spec.startsWith("@")) {
|
|
388
|
+
const parts = spec.split("/");
|
|
389
|
+
if (parts.length < 2) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
return `${parts[0]}/${parts[1].replace(/@[^@]+$/, "")}`;
|
|
393
|
+
}
|
|
394
|
+
return spec.replace(/@[^@]+$/, "");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* @param {string} packageSpec
|
|
399
|
+
* @returns {{ ok: boolean, package: string|null, packageSpec: string, currentVersion: string|null, latestVersion: string|null, current: boolean|null, diagnostics: any[] }}
|
|
400
|
+
*/
|
|
401
|
+
export function checkTemplatePackageStatus(packageSpec) {
|
|
402
|
+
const packageName = registryPackageNameFromSpec(packageSpec);
|
|
403
|
+
if (!packageName) {
|
|
404
|
+
return {
|
|
405
|
+
ok: true,
|
|
406
|
+
package: null,
|
|
407
|
+
packageSpec,
|
|
408
|
+
currentVersion: null,
|
|
409
|
+
latestVersion: null,
|
|
410
|
+
current: null,
|
|
411
|
+
diagnostics: []
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const access = checkDoctorPackageAccess(packageSpec);
|
|
415
|
+
const latest = checkDoctorPackageAccess(`${packageName}@latest`);
|
|
416
|
+
const currentVersion = access.checkedVersion;
|
|
417
|
+
const latestVersion = latest.checkedVersion;
|
|
418
|
+
return {
|
|
419
|
+
ok: access.ok && latest.ok,
|
|
420
|
+
package: packageName,
|
|
421
|
+
packageSpec,
|
|
422
|
+
currentVersion,
|
|
423
|
+
latestVersion,
|
|
424
|
+
current: currentVersion && latestVersion ? currentVersion === latestVersion : null,
|
|
425
|
+
diagnostics: [...access.diagnostics, ...latest.diagnostics]
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* @param {string} packageSpec
|
|
431
|
+
* @returns {{ checked: false, ok: null, package: string|null, packageSpec: string, currentVersion: null, latestVersion: null, current: null, reason: string, diagnostics: any[] }}
|
|
432
|
+
*/
|
|
433
|
+
export function localTemplatePackageStatus(packageSpec) {
|
|
434
|
+
return {
|
|
435
|
+
checked: false,
|
|
436
|
+
ok: null,
|
|
437
|
+
package: registryPackageNameFromSpec(packageSpec),
|
|
438
|
+
packageSpec,
|
|
439
|
+
currentVersion: null,
|
|
440
|
+
latestVersion: null,
|
|
441
|
+
current: null,
|
|
442
|
+
reason: "Package registry checks were skipped because --local was used.",
|
|
443
|
+
diagnostics: []
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @param {string} packageSpec
|
|
449
|
+
* @param {{ stdout?: string, stderr?: string, error?: Error }} result
|
|
450
|
+
* @returns {any}
|
|
451
|
+
*/
|
|
452
|
+
function doctorPackageDiagnostic(packageSpec, result) {
|
|
453
|
+
const diagnostic = catalogDoctorPackageDiagnostic({
|
|
454
|
+
id: CLI_PACKAGE_NAME,
|
|
455
|
+
kind: "package",
|
|
456
|
+
package: CLI_PACKAGE_NAME,
|
|
457
|
+
defaultVersion: packageSpec.slice(`${CLI_PACKAGE_NAME}@`.length)
|
|
458
|
+
}, packageSpec, result);
|
|
459
|
+
return {
|
|
460
|
+
...diagnostic,
|
|
461
|
+
code: diagnostic.code.replace(/^catalog_package_/, "package_registry_"),
|
|
462
|
+
path: CLI_PACKAGE_NAME
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* @param {string} left
|
|
468
|
+
* @param {string} right
|
|
469
|
+
* @returns {number}
|
|
470
|
+
*/
|
|
471
|
+
function compareSemver(left, right) {
|
|
472
|
+
const leftParts = left.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
|
|
473
|
+
const rightParts = right.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
|
|
474
|
+
for (let index = 0; index < 3; index += 1) {
|
|
475
|
+
if (leftParts[index] > rightParts[index]) return 1;
|
|
476
|
+
if (leftParts[index] < rightParts[index]) return -1;
|
|
477
|
+
}
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* @param {string[]} args
|
|
483
|
+
* @param {string} cwd
|
|
484
|
+
* @returns {any}
|
|
485
|
+
*/
|
|
486
|
+
export function runNpmForPackageUpdate(args, cwd) {
|
|
487
|
+
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
488
|
+
return childProcess.spawnSync(npmBin, args, {
|
|
489
|
+
cwd,
|
|
490
|
+
encoding: "utf8",
|
|
491
|
+
env: {
|
|
492
|
+
...process.env,
|
|
493
|
+
...localNpmrcEnv(cwd),
|
|
494
|
+
PATH: process.env.PATH || ""
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* @param {string} cwd
|
|
501
|
+
* @returns {string}
|
|
502
|
+
*/
|
|
503
|
+
export function latestTopogramCliVersion(cwd) {
|
|
504
|
+
const result = runNpmForPackageUpdate(["view", "--json", `--registry=${NPMJS_REGISTRY}`, "--", CLI_PACKAGE_NAME, "version"], cwd);
|
|
505
|
+
if (result.status !== 0) {
|
|
506
|
+
throw new Error(formatPackageUpdateNpmError(`${CLI_PACKAGE_NAME}@latest`, "inspect", result));
|
|
507
|
+
}
|
|
508
|
+
const raw = String(result.stdout || "").trim();
|
|
509
|
+
const version = raw.startsWith("\"") ? JSON.parse(raw) : raw;
|
|
510
|
+
if (!isPackageVersion(version)) {
|
|
511
|
+
throw new Error(`npm returned invalid latest version '${version || "(empty)"}' for ${CLI_PACKAGE_NAME}.`);
|
|
512
|
+
}
|
|
513
|
+
return version;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* @param {string} cwd
|
|
518
|
+
* @param {Array<Record<string, any>>} diagnostics
|
|
519
|
+
* @returns {string}
|
|
520
|
+
*/
|
|
521
|
+
function resolveLatestTopogramCliVersionForPackageUpdate(cwd, diagnostics) {
|
|
522
|
+
try {
|
|
523
|
+
return latestTopogramCliVersion(cwd);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
throw new Error(messageFromError(error));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* @param {string} cwd
|
|
531
|
+
* @param {string} version
|
|
532
|
+
* @returns {{ updated: boolean, path: string|null }}
|
|
533
|
+
*/
|
|
534
|
+
function writeTopogramCliVersionConventionIfPresent(cwd, version) {
|
|
535
|
+
const versionPath = path.join(cwd, "topogram-cli.version");
|
|
536
|
+
if (!fs.existsSync(versionPath)) {
|
|
537
|
+
return { updated: false, path: null };
|
|
538
|
+
}
|
|
539
|
+
fs.writeFileSync(versionPath, `${version}\n`, "utf8");
|
|
540
|
+
return { updated: true, path: versionPath };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Remove stale tarball metadata for the CLI package before npm installs the
|
|
545
|
+
* requested version. npm package registry can repack publish metadata, so copying a
|
|
546
|
+
* local npm-pack resolved URL or integrity into a consumer lockfile can make
|
|
547
|
+
* npm ci fail with a checksum mismatch.
|
|
548
|
+
*
|
|
549
|
+
* @param {string} cwd
|
|
550
|
+
* @param {string} version
|
|
551
|
+
* @returns {boolean}
|
|
552
|
+
*/
|
|
553
|
+
function sanitizeTopogramLockForPackageUpdate(cwd, version) {
|
|
554
|
+
const lockPath = path.join(cwd, "package-lock.json");
|
|
555
|
+
if (!fs.existsSync(lockPath)) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
559
|
+
const packages = lock && typeof lock === "object" && lock.packages && typeof lock.packages === "object"
|
|
560
|
+
? lock.packages
|
|
561
|
+
: null;
|
|
562
|
+
const packageEntry = packages?.[`node_modules/${CLI_PACKAGE_NAME}`];
|
|
563
|
+
if (!packageEntry || typeof packageEntry !== "object" || packageEntry.version !== version) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
let changed = false;
|
|
567
|
+
for (const key of ["resolved", "integrity"]) {
|
|
568
|
+
if (Object.prototype.hasOwnProperty.call(packageEntry, key)) {
|
|
569
|
+
delete packageEntry[key];
|
|
570
|
+
changed = true;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (changed) {
|
|
574
|
+
fs.writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
|
|
575
|
+
}
|
|
576
|
+
return changed;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* @param {string} cwd
|
|
581
|
+
* @returns {{ checked: boolean, path: string, packageVersion: string|null, dependencySpec: string|null, hasTarballMetadata: boolean, resolvedVersion: string|null, refreshRecommended: boolean, diagnostics: Array<Record<string, any>> }}
|
|
582
|
+
*/
|
|
583
|
+
export function inspectTopogramCliLockfile(cwd) {
|
|
584
|
+
const lockPath = path.join(cwd, "package-lock.json");
|
|
585
|
+
/** @type {{ checked: boolean, path: string, packageVersion: string|null, dependencySpec: string|null, hasTarballMetadata: boolean, resolvedVersion: string|null, refreshRecommended: boolean, diagnostics: Array<Record<string, any>> }} */
|
|
586
|
+
const result = {
|
|
587
|
+
checked: false,
|
|
588
|
+
path: lockPath,
|
|
589
|
+
packageVersion: null,
|
|
590
|
+
dependencySpec: readProjectCliDependencySpec(cwd),
|
|
591
|
+
hasTarballMetadata: false,
|
|
592
|
+
resolvedVersion: null,
|
|
593
|
+
refreshRecommended: false,
|
|
594
|
+
diagnostics: []
|
|
595
|
+
};
|
|
596
|
+
if (!fs.existsSync(lockPath)) {
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
result.checked = true;
|
|
600
|
+
try {
|
|
601
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
602
|
+
const entry = lock?.packages?.[`node_modules/${CLI_PACKAGE_NAME}`];
|
|
603
|
+
if (!entry || typeof entry !== "object") {
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
result.packageVersion = typeof entry.version === "string" ? entry.version : null;
|
|
607
|
+
const resolved = typeof entry.resolved === "string" ? entry.resolved : null;
|
|
608
|
+
result.resolvedVersion = resolved ? resolvedTopogramCliVersion(resolved) : null;
|
|
609
|
+
result.hasTarballMetadata = Object.prototype.hasOwnProperty.call(entry, "resolved") ||
|
|
610
|
+
Object.prototype.hasOwnProperty.call(entry, "integrity");
|
|
611
|
+
const conventionVersion = readTopogramCliVersionConvention(cwd);
|
|
612
|
+
const resolvedVersionMismatch = Boolean(result.packageVersion && result.resolvedVersion && result.resolvedVersion !== result.packageVersion);
|
|
613
|
+
const normalizedResolved = normalizeRegistryUrl(resolved);
|
|
614
|
+
const normalizedRegistry = normalizeRegistryUrl(NPMJS_REGISTRY) || NPMJS_REGISTRY;
|
|
615
|
+
const npmjsTarball = Boolean(normalizedResolved && normalizedResolved.startsWith(`${normalizedRegistry}/`));
|
|
616
|
+
const localTarballMetadata = Boolean(resolved && (
|
|
617
|
+
/^file:/.test(resolved) ||
|
|
618
|
+
(!npmjsTarball && /\.tgz(?:$|[?#])/.test(resolved))
|
|
619
|
+
));
|
|
620
|
+
result.refreshRecommended = Boolean(
|
|
621
|
+
result.packageVersion &&
|
|
622
|
+
conventionVersion &&
|
|
623
|
+
conventionVersion === result.packageVersion &&
|
|
624
|
+
(resolvedVersionMismatch || localTarballMetadata)
|
|
625
|
+
);
|
|
626
|
+
if (result.refreshRecommended) {
|
|
627
|
+
result.diagnostics.push({
|
|
628
|
+
code: "topogram_cli_lockfile_refresh_available",
|
|
629
|
+
severity: "warning",
|
|
630
|
+
message: "package-lock.json contains stale Topogram CLI tarball metadata for the pinned version.",
|
|
631
|
+
path: lockPath,
|
|
632
|
+
suggestedFix: `Run \`topogram package update-cli ${result.packageVersion}\` to refresh from npm registry metadata.`
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
} catch (error) {
|
|
636
|
+
result.diagnostics.push({
|
|
637
|
+
code: "topogram_cli_lockfile_unreadable",
|
|
638
|
+
severity: "warning",
|
|
639
|
+
message: `Could not inspect package-lock.json: ${messageFromError(error)}`,
|
|
640
|
+
path: lockPath,
|
|
641
|
+
suggestedFix: "Regenerate package-lock.json with npm install."
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* @param {string} resolved
|
|
649
|
+
* @returns {string|null}
|
|
650
|
+
*/
|
|
651
|
+
function resolvedTopogramCliVersion(resolved) {
|
|
652
|
+
const match = resolved.match(/\/@topogram\/cli\/-\/cli-([^/.?#]+(?:\.[^/.?#]+){2}(?:[-+][^/?#]+)?)\.tgz/);
|
|
653
|
+
return match ? match[1] : null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* @param {string} cwd
|
|
658
|
+
* @returns {string|null}
|
|
659
|
+
*/
|
|
660
|
+
function readTopogramCliVersionConvention(cwd) {
|
|
661
|
+
const versionPath = path.join(cwd, "topogram-cli.version");
|
|
662
|
+
if (!fs.existsSync(versionPath)) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
const value = fs.readFileSync(versionPath, "utf8").trim();
|
|
666
|
+
return value || null;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* @param {string} cwd
|
|
671
|
+
* @returns {Record<string, any>}
|
|
672
|
+
*/
|
|
673
|
+
function readPackageJsonForUpdate(cwd) {
|
|
674
|
+
const packagePath = path.join(cwd, "package.json");
|
|
675
|
+
if (!fs.existsSync(packagePath)) {
|
|
676
|
+
throw new Error("topogram package update-cli must be run from a package directory with package.json.");
|
|
677
|
+
}
|
|
678
|
+
return JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* @param {string} cwd
|
|
683
|
+
* @param {string} version
|
|
684
|
+
* @param {string} dependencySpec
|
|
685
|
+
* @returns {{ packageJsonUpdated: boolean, lockfileUpdated: boolean }}
|
|
686
|
+
*/
|
|
687
|
+
function updateTopogramCliDependencyFiles(cwd, version, dependencySpec) {
|
|
688
|
+
const packagePath = path.join(cwd, "package.json");
|
|
689
|
+
const packageJson = readPackageJsonForUpdate(cwd);
|
|
690
|
+
const hasDevDependency = packageJson.devDependencies &&
|
|
691
|
+
typeof packageJson.devDependencies === "object" &&
|
|
692
|
+
Object.prototype.hasOwnProperty.call(packageJson.devDependencies, CLI_PACKAGE_NAME);
|
|
693
|
+
const hasDependency = packageJson.dependencies &&
|
|
694
|
+
typeof packageJson.dependencies === "object" &&
|
|
695
|
+
Object.prototype.hasOwnProperty.call(packageJson.dependencies, CLI_PACKAGE_NAME);
|
|
696
|
+
const hasVersionConvention = fs.existsSync(path.join(cwd, "topogram-cli.version"));
|
|
697
|
+
const shouldUpdatePackageJson = hasDevDependency || hasDependency || !hasVersionConvention;
|
|
698
|
+
if (!shouldUpdatePackageJson) {
|
|
699
|
+
return { packageJsonUpdated: false, lockfileUpdated: false };
|
|
700
|
+
}
|
|
701
|
+
if (hasDependency && !hasDevDependency) {
|
|
702
|
+
packageJson.dependencies[CLI_PACKAGE_NAME] = dependencySpec.slice(`${CLI_PACKAGE_NAME}@`.length);
|
|
703
|
+
} else {
|
|
704
|
+
packageJson.devDependencies = packageJson.devDependencies && typeof packageJson.devDependencies === "object"
|
|
705
|
+
? packageJson.devDependencies
|
|
706
|
+
: {};
|
|
707
|
+
packageJson.devDependencies[CLI_PACKAGE_NAME] = dependencySpec.slice(`${CLI_PACKAGE_NAME}@`.length);
|
|
708
|
+
}
|
|
709
|
+
if (hasDevDependency && packageJson.dependencies && typeof packageJson.dependencies === "object") {
|
|
710
|
+
delete packageJson.dependencies[CLI_PACKAGE_NAME];
|
|
711
|
+
}
|
|
712
|
+
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
|
|
713
|
+
|
|
714
|
+
const lockPath = path.join(cwd, "package-lock.json");
|
|
715
|
+
if (!fs.existsSync(lockPath)) {
|
|
716
|
+
return { packageJsonUpdated: true, lockfileUpdated: false };
|
|
717
|
+
}
|
|
718
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
719
|
+
lock.packages = lock.packages && typeof lock.packages === "object" ? lock.packages : {};
|
|
720
|
+
lock.packages[""] = lock.packages[""] && typeof lock.packages[""] === "object" ? lock.packages[""] : {};
|
|
721
|
+
const rootEntry = lock.packages[""];
|
|
722
|
+
const lockHasDependency = rootEntry.dependencies &&
|
|
723
|
+
typeof rootEntry.dependencies === "object" &&
|
|
724
|
+
Object.prototype.hasOwnProperty.call(rootEntry.dependencies, CLI_PACKAGE_NAME);
|
|
725
|
+
if (lockHasDependency && !hasDevDependency) {
|
|
726
|
+
rootEntry.dependencies[CLI_PACKAGE_NAME] = dependencySpec.slice(`${CLI_PACKAGE_NAME}@`.length);
|
|
727
|
+
} else {
|
|
728
|
+
rootEntry.devDependencies = rootEntry.devDependencies && typeof rootEntry.devDependencies === "object"
|
|
729
|
+
? rootEntry.devDependencies
|
|
730
|
+
: {};
|
|
731
|
+
rootEntry.devDependencies[CLI_PACKAGE_NAME] = dependencySpec.slice(`${CLI_PACKAGE_NAME}@`.length);
|
|
732
|
+
}
|
|
733
|
+
if ((hasDevDependency || !lockHasDependency) && rootEntry.dependencies && typeof rootEntry.dependencies === "object") {
|
|
734
|
+
delete lock.packages[""].dependencies[CLI_PACKAGE_NAME];
|
|
735
|
+
}
|
|
736
|
+
const entryPath = `node_modules/${CLI_PACKAGE_NAME}`;
|
|
737
|
+
const existingEntry = lock.packages[entryPath] && typeof lock.packages[entryPath] === "object"
|
|
738
|
+
? lock.packages[entryPath]
|
|
739
|
+
: {};
|
|
740
|
+
lock.packages[entryPath] = {
|
|
741
|
+
...existingEntry,
|
|
742
|
+
version,
|
|
743
|
+
dev: true,
|
|
744
|
+
license: existingEntry.license || "Apache-2.0",
|
|
745
|
+
bin: existingEntry.bin || { topogram: "src/cli.js" },
|
|
746
|
+
engines: existingEntry.engines || { node: ">=20" }
|
|
747
|
+
};
|
|
748
|
+
delete lock.packages[entryPath].resolved;
|
|
749
|
+
delete lock.packages[entryPath].integrity;
|
|
750
|
+
fs.writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
|
|
751
|
+
return { packageJsonUpdated: true, lockfileUpdated: true };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* @param {string} value
|
|
756
|
+
* @returns {boolean}
|
|
757
|
+
*/
|
|
758
|
+
export function isPackageVersion(value) {
|
|
759
|
+
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(value);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* @param {any} result
|
|
764
|
+
* @returns {boolean}
|
|
765
|
+
*/
|
|
766
|
+
function isPackageUpdateNpmAuthFailure(result) {
|
|
767
|
+
const output = [result.error?.message, result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
768
|
+
const normalized = output.toLowerCase();
|
|
769
|
+
return /\b(e401|eneedauth)\b/.test(normalized) ||
|
|
770
|
+
normalized.includes("unauthenticated") ||
|
|
771
|
+
normalized.includes("authentication required");
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* @param {string} spec
|
|
776
|
+
* @param {"inspect"|"install"|"check"} step
|
|
777
|
+
* @param {any} result
|
|
778
|
+
* @returns {string}
|
|
779
|
+
*/
|
|
780
|
+
function formatPackageUpdateNpmError(spec, step, result) {
|
|
781
|
+
const output = [result.error?.message, result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
782
|
+
const normalized = output.toLowerCase();
|
|
783
|
+
if (result.error?.code === "ENOENT") {
|
|
784
|
+
return "npm was not found. Install Node.js/npm and retry.";
|
|
785
|
+
}
|
|
786
|
+
if (isPackageUpdateNpmAuthFailure(result)) {
|
|
787
|
+
return [
|
|
788
|
+
`Authentication is required to ${step} ${spec}.`,
|
|
789
|
+
"Configure registry-specific npm auth when using private packages.",
|
|
790
|
+
output
|
|
791
|
+
].filter(Boolean).join("\n");
|
|
792
|
+
}
|
|
793
|
+
if (/\be403\b/.test(normalized) || normalized.includes("forbidden") || normalized.includes("permission")) {
|
|
794
|
+
return [
|
|
795
|
+
`Package access was denied while trying to ${step} ${spec}.`,
|
|
796
|
+
"Check npm package registry read access for the consumer environment.",
|
|
797
|
+
output
|
|
798
|
+
].filter(Boolean).join("\n");
|
|
799
|
+
}
|
|
800
|
+
if (/\b(e404|404)\b/.test(normalized) || normalized.includes("not found")) {
|
|
801
|
+
return [
|
|
802
|
+
`${spec} was not found, or the current token does not have access to it.`,
|
|
803
|
+
"Check the package version and npm package registry access.",
|
|
804
|
+
output
|
|
805
|
+
].filter(Boolean).join("\n");
|
|
806
|
+
}
|
|
807
|
+
if (/\beintegrity\b/.test(normalized) || normalized.includes("integrity checksum failed")) {
|
|
808
|
+
return [
|
|
809
|
+
`Package integrity failed while trying to ${step} ${spec}.`,
|
|
810
|
+
"Regenerate package-lock.json from the published npm package registry tarball.",
|
|
811
|
+
output
|
|
812
|
+
].filter(Boolean).join("\n");
|
|
813
|
+
}
|
|
814
|
+
return `Failed to ${step} ${spec}.\n${output || "unknown error"}`.trim();
|
|
815
|
+
}
|