@getmagical/cli 0.1.7 → 0.1.8

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/README.md CHANGED
@@ -68,6 +68,10 @@ Once installed, users can update from the same channel with:
68
68
  mgcl --update
69
69
  ```
70
70
 
71
+ Installed CLIs also check the configured npm dist-tag before running
72
+ subcommands. If the local version is behind, `mgcl` blocks the command and
73
+ asks the user to run `mgcl update` before rerunning it.
74
+
71
75
  To remove the managed install:
72
76
 
73
77
  ```bash
package/dist/index.js CHANGED
@@ -2546,6 +2546,9 @@ function isAppErrorCategory(category) {
2546
2546
  return typeof category === "string" && // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2547
2547
  ORDERED_APP_ERROR_CATEGORIES.includes(category);
2548
2548
  }
2549
+ function isNonEmptyAppErrorCategoryArray(categories) {
2550
+ return Array.isArray(categories) && categories.length > 0 && categories.every(isAppErrorCategory);
2551
+ }
2549
2552
  var APP_ERROR_SEVERITIES = ["warn", "error", "fatal"];
2550
2553
  function isAppErrorCode(code) {
2551
2554
  return typeof code === "number" && Number.isInteger(code) && code >= 400 && code <= 599;
@@ -2583,42 +2586,55 @@ var AppError = class _AppError extends Error {
2583
2586
  }
2584
2587
  return type;
2585
2588
  }
2586
- static extractCategoriesFromCause(cause) {
2587
- if (!cause || typeof cause !== "object" || !("categories" in cause)) {
2588
- return ["internal_platform"];
2589
+ static getInheritedCauseField(cause, extractor, seen = /* @__PURE__ */ new Set()) {
2590
+ if (!cause || typeof cause !== "object" || seen.has(cause)) {
2591
+ return void 0;
2589
2592
  }
2590
- if (Array.isArray(cause.categories) && cause.categories.length > 0 && cause.categories.every(isAppErrorCategory)) {
2591
- return cause.categories;
2593
+ const causeRecord = cause;
2594
+ const extractedValue = extractor(causeRecord);
2595
+ if (extractedValue !== void 0) {
2596
+ return extractedValue;
2592
2597
  }
2593
- return ["internal_platform"];
2598
+ return _AppError.getInheritedCauseField(causeRecord["cause"], extractor, new Set(seen).add(cause));
2599
+ }
2600
+ static extractCategoriesFromCause(cause) {
2601
+ return _AppError.getInheritedCauseField(cause, (causeRecord) => {
2602
+ const { categories } = causeRecord;
2603
+ if (isNonEmptyAppErrorCategoryArray(categories)) {
2604
+ return categories;
2605
+ }
2606
+ return void 0;
2607
+ }) ?? ["internal_platform"];
2594
2608
  }
2595
2609
  static extractSeverityFromCause(cause) {
2596
- if (cause && typeof cause === "object" && "severity" in cause && isAppErrorSeverity(cause.severity)) {
2597
- return cause.severity;
2598
- }
2599
- return "error";
2610
+ return _AppError.getInheritedCauseField(cause, (causeRecord) => {
2611
+ const { severity } = causeRecord;
2612
+ return isAppErrorSeverity(severity) ? severity : void 0;
2613
+ }) ?? "error";
2600
2614
  }
2601
2615
  static extractStatusCodeFromCause(cause) {
2602
- if (cause && typeof cause === "object" && "statusCode" in cause && isAppErrorCode(cause.statusCode)) {
2603
- return cause.statusCode;
2604
- }
2605
- if (cause && typeof cause === "object" && "code" in cause && isAppErrorCode(cause.code)) {
2606
- return cause.code;
2607
- }
2608
- return void 0;
2616
+ return _AppError.getInheritedCauseField(cause, (causeRecord) => {
2617
+ const { statusCode } = causeRecord;
2618
+ if (isAppErrorCode(statusCode)) {
2619
+ return statusCode;
2620
+ }
2621
+ const { code: legacyCode } = causeRecord;
2622
+ return isAppErrorCode(legacyCode) ? legacyCode : void 0;
2623
+ });
2609
2624
  }
2610
2625
  /** @deprecated Use statusCode instead. */
2611
2626
  get code() {
2612
2627
  return this.statusCode;
2613
2628
  }
2614
2629
  static extractSuggestedActionFromCause(cause) {
2615
- if (cause && typeof cause === "object" && "suggestedAction" in cause && typeof cause.suggestedAction === "string") {
2616
- const trimmedSuggestedAction = cause.suggestedAction.trim();
2617
- if (trimmedSuggestedAction.length > 0) {
2618
- return trimmedSuggestedAction;
2630
+ return _AppError.getInheritedCauseField(cause, (causeRecord) => {
2631
+ const { suggestedAction } = causeRecord;
2632
+ if (typeof suggestedAction !== "string") {
2633
+ return void 0;
2619
2634
  }
2620
- }
2621
- return void 0;
2635
+ const trimmedSuggestedAction = suggestedAction.trim();
2636
+ return trimmedSuggestedAction.length > 0 ? trimmedSuggestedAction : void 0;
2637
+ });
2622
2638
  }
2623
2639
  static applyStack(error, stack, internalFrames) {
2624
2640
  if (stack) {
@@ -3022,10 +3038,28 @@ import { spawn as spawn2 } from "child_process";
3022
3038
  import fs from "fs";
3023
3039
  import path3 from "path";
3024
3040
  import { fileURLToPath } from "url";
3041
+
3042
+ // ../common/dist/src/stdlib/url.utils.js
3043
+ function safeUrlConstructor(url) {
3044
+ try {
3045
+ return Ok(new URL(url));
3046
+ } catch {
3047
+ return Err({
3048
+ type: "invalid-url",
3049
+ message: `Invalid URL`,
3050
+ url
3051
+ });
3052
+ }
3053
+ }
3054
+
3055
+ // src/self-update.ts
3025
3056
  var CLI_PACKAGE_NAME = "@getmagical/cli";
3026
3057
  var DEFAULT_CLI_DIST_TAG2 = "latest";
3027
3058
  var DEFAULT_NPM_REGISTRY_URL = "https://registry.npmjs.org/";
3028
3059
  var MANAGED_NODE_SUBPATH = "runtime/node";
3060
+ function isRecord2(value) {
3061
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3062
+ }
3029
3063
  function resolveInstalledPackageRoot({
3030
3064
  entryFilePath,
3031
3065
  packageName
@@ -3066,6 +3100,154 @@ function resolveInstallPrefixFromPackageRoot(packageRootPath) {
3066
3100
  currentPath = parentPath;
3067
3101
  }
3068
3102
  }
3103
+ function readInstalledPackageVersion({
3104
+ packageName,
3105
+ packageRootPath
3106
+ }) {
3107
+ const packageManifestPath = path3.join(packageRootPath, "package.json");
3108
+ if (!fs.existsSync(packageManifestPath)) {
3109
+ throw new CliError(
3110
+ `Installed package metadata was not found at ${packageManifestPath}. Reinstall mgcl and try again.`
3111
+ );
3112
+ }
3113
+ const packageManifestResult = safeJsonParse(
3114
+ fs.readFileSync(packageManifestPath, "utf8")
3115
+ );
3116
+ if (isErr(packageManifestResult)) {
3117
+ throw new CliError(`Failed to parse ${packageManifestPath}.`, {
3118
+ cause: packageManifestResult.error.cause
3119
+ });
3120
+ }
3121
+ const packageManifest = packageManifestResult.value;
3122
+ if (packageManifest.name !== packageName) {
3123
+ throw new CliError(
3124
+ `Expected installed package ${packageName} at ${packageManifestPath}. Reinstall mgcl and try again.`
3125
+ );
3126
+ }
3127
+ if (typeof packageManifest.version !== "string" || !packageManifest.version) {
3128
+ throw new CliError(
3129
+ `Installed package version is missing from ${packageManifestPath}. Reinstall mgcl and try again.`
3130
+ );
3131
+ }
3132
+ return packageManifest.version;
3133
+ }
3134
+ function parseVersionSegment(segment, version) {
3135
+ if (!/^\d+$/u.test(segment)) {
3136
+ throw new CliError(`Invalid CLI version "${version}".`);
3137
+ }
3138
+ return Number(segment);
3139
+ }
3140
+ function parseSemanticVersion(version) {
3141
+ const normalizedVersion = version.trim().replace(/^v/u, "");
3142
+ const [versionWithoutBuildMetadata = ""] = normalizedVersion.split("+");
3143
+ if (!versionWithoutBuildMetadata) {
3144
+ throw new CliError(`Invalid CLI version "${version}".`);
3145
+ }
3146
+ const [releasePart, prereleasePart] = versionWithoutBuildMetadata.split("-");
3147
+ if (!releasePart) {
3148
+ throw new CliError(`Invalid CLI version "${version}".`);
3149
+ }
3150
+ return {
3151
+ prerelease: prereleasePart ? prereleasePart.split(".") : null,
3152
+ release: releasePart.split(".").map((segment) => parseVersionSegment(segment, version))
3153
+ };
3154
+ }
3155
+ function comparePrereleaseIdentifiers(left, right) {
3156
+ const leftIsNumeric = /^\d+$/u.test(left);
3157
+ const rightIsNumeric = /^\d+$/u.test(right);
3158
+ if (leftIsNumeric && rightIsNumeric) {
3159
+ return Number(left) - Number(right);
3160
+ }
3161
+ if (leftIsNumeric) {
3162
+ return -1;
3163
+ }
3164
+ if (rightIsNumeric) {
3165
+ return 1;
3166
+ }
3167
+ return left.localeCompare(right);
3168
+ }
3169
+ function compareSemanticVersions(leftVersion, rightVersion) {
3170
+ const left = parseSemanticVersion(leftVersion);
3171
+ const right = parseSemanticVersion(rightVersion);
3172
+ const releaseLength = Math.max(left.release.length, right.release.length);
3173
+ for (let index = 0; index < releaseLength; index += 1) {
3174
+ const difference = (left.release[index] ?? 0) - (right.release[index] ?? 0);
3175
+ if (difference !== 0) {
3176
+ return difference;
3177
+ }
3178
+ }
3179
+ if (!left.prerelease && !right.prerelease) {
3180
+ return 0;
3181
+ }
3182
+ if (!left.prerelease) {
3183
+ return 1;
3184
+ }
3185
+ if (!right.prerelease) {
3186
+ return -1;
3187
+ }
3188
+ const prereleaseLength = Math.max(
3189
+ left.prerelease.length,
3190
+ right.prerelease.length
3191
+ );
3192
+ for (let index = 0; index < prereleaseLength; index += 1) {
3193
+ const leftIdentifier = left.prerelease[index];
3194
+ const rightIdentifier = right.prerelease[index];
3195
+ if (leftIdentifier === void 0) {
3196
+ return -1;
3197
+ }
3198
+ if (rightIdentifier === void 0) {
3199
+ return 1;
3200
+ }
3201
+ const difference = comparePrereleaseIdentifiers(
3202
+ leftIdentifier,
3203
+ rightIdentifier
3204
+ );
3205
+ if (difference !== 0) {
3206
+ return difference;
3207
+ }
3208
+ }
3209
+ return 0;
3210
+ }
3211
+ async function fetchPublishedPackageVersion({
3212
+ distTag,
3213
+ fetchFn = fetch,
3214
+ packageName,
3215
+ registryUrl
3216
+ }) {
3217
+ const distTagUrlResult = safeUrlConstructor(
3218
+ `${registryUrl.replace(/\/?$/u, "/")}-/package/${encodeURIComponent(packageName)}/dist-tags`
3219
+ );
3220
+ if (isErr(distTagUrlResult)) {
3221
+ throw new CliError(`Invalid npm registry URL "${registryUrl}".`);
3222
+ }
3223
+ const response = await fetchFn(distTagUrlResult.value, {
3224
+ headers: {
3225
+ accept: "application/json"
3226
+ }
3227
+ });
3228
+ if (!response.ok) {
3229
+ throw new CliError(
3230
+ `Could not check the latest published mgcl version (status ${response.status}).`
3231
+ );
3232
+ }
3233
+ const distTagsResult = safeJsonParse(await response.text());
3234
+ if (isErr(distTagsResult)) {
3235
+ throw new CliError("Could not parse the npm dist-tags response for mgcl.", {
3236
+ cause: distTagsResult.error.cause
3237
+ });
3238
+ }
3239
+ if (!isRecord2(distTagsResult.value)) {
3240
+ throw new CliError("Could not parse the npm dist-tags response for mgcl.");
3241
+ }
3242
+ const distTags = distTagsResult.value;
3243
+ const publishedVersion = distTags[distTag];
3244
+ if (typeof publishedVersion !== "string" || publishedVersion.length === 0) {
3245
+ throw new CliError(
3246
+ `Could not find npm dist-tag "${distTag}" for ${packageName}.`
3247
+ );
3248
+ }
3249
+ return publishedVersion;
3250
+ }
3069
3251
  function resolveNpmCommandFromPath() {
3070
3252
  return {
3071
3253
  argsPrefix: [],
@@ -3179,18 +3361,44 @@ async function selfUpdateInstalledCli({
3179
3361
  registryUrl
3180
3362
  });
3181
3363
  }
3182
-
3183
- // ../common/dist/src/stdlib/url.utils.js
3184
- function safeUrlConstructor(url) {
3185
- try {
3186
- return Ok(new URL(url));
3187
- } catch {
3188
- return Err({
3189
- type: "invalid-url",
3190
- message: `Invalid URL`,
3191
- url
3192
- });
3364
+ async function getInstalledCliUpdateRequirement({
3365
+ distTag = DEFAULT_CLI_DIST_TAG2,
3366
+ entryFilePath = fileURLToPath(import.meta.url),
3367
+ fetchFn = fetch,
3368
+ packageName = CLI_PACKAGE_NAME,
3369
+ registryUrl = DEFAULT_NPM_REGISTRY_URL
3370
+ } = {}) {
3371
+ const packageRootPath = resolveInstalledPackageRoot({
3372
+ entryFilePath,
3373
+ packageName
3374
+ });
3375
+ if (!packageRootPath) {
3376
+ return null;
3377
+ }
3378
+ const installedVersion = readInstalledPackageVersion({
3379
+ packageName,
3380
+ packageRootPath
3381
+ });
3382
+ const publishedVersionResult = await fromThrowable(
3383
+ async () => fetchPublishedPackageVersion({
3384
+ distTag,
3385
+ fetchFn,
3386
+ packageName,
3387
+ registryUrl
3388
+ }),
3389
+ "cli-update-check-failed"
3390
+ );
3391
+ if (isErr(publishedVersionResult)) {
3392
+ return null;
3393
+ }
3394
+ const publishedVersion = publishedVersionResult.value;
3395
+ if (compareSemanticVersions(installedVersion, publishedVersion) >= 0) {
3396
+ return null;
3193
3397
  }
3398
+ return {
3399
+ installedVersion,
3400
+ latestVersion: publishedVersion
3401
+ };
3194
3402
  }
3195
3403
 
3196
3404
  // src/settings-command.ts
@@ -3555,7 +3763,7 @@ function parseCommandOptionValue(rawValue, optionSpec) {
3555
3763
  }
3556
3764
  }
3557
3765
  }
3558
- function isRecord2(value) {
3766
+ function isRecord3(value) {
3559
3767
  return typeof value === "object" && value !== null && !Array.isArray(value);
3560
3768
  }
3561
3769
  function resolveOrganizationBySelector({
@@ -3615,7 +3823,7 @@ function collectOperationVariables(operationSpec, options) {
3615
3823
  parsedVariables.error.issues[0]?.message ?? "Invalid command input."
3616
3824
  );
3617
3825
  }
3618
- if (!isRecord2(parsedVariables.data)) {
3826
+ if (!isRecord3(parsedVariables.data)) {
3619
3827
  throw new CliError("Invalid command input.");
3620
3828
  }
3621
3829
  return parsedVariables.data;
@@ -3718,6 +3926,25 @@ async function handleSelfUpdate(dependencies, options) {
3718
3926
  `Updated mgcl via npm (${updateResult.packageSpecifier}) in ${updateResult.installPrefix}.`
3719
3927
  );
3720
3928
  }
3929
+ function isRootUpdateCommand(program2, actionCommand) {
3930
+ return actionCommand.parent === program2 && actionCommand.name() === "update";
3931
+ }
3932
+ async function enforceLatestCliVersionIfNeeded({
3933
+ dependencies,
3934
+ program: program2,
3935
+ actionCommand
3936
+ }) {
3937
+ if (actionCommand === program2 || isRootUpdateCommand(program2, actionCommand)) {
3938
+ return;
3939
+ }
3940
+ const updateRequirement = await dependencies.ensureLatestCliVersion?.();
3941
+ if (!updateRequirement) {
3942
+ return;
3943
+ }
3944
+ throw new CliError(
3945
+ `mgcl ${updateRequirement.installedVersion} is outdated. Upgrade to ${updateRequirement.latestVersion} with "mgcl update", then rerun your command.`
3946
+ );
3947
+ }
3721
3948
  async function handleOperationCommand(dependencies, operationSpec, commandOptions) {
3722
3949
  const state = requireLoggedInState(await dependencies.stateStore.load());
3723
3950
  const organization = resolveSelectedOrganization({
@@ -3862,6 +4089,7 @@ function createDefaultDependencies() {
3862
4089
  refreshTokenStore: createKeytarRefreshTokenStore(
3863
4090
  runtimeConfig.keychainServiceName
3864
4091
  ),
4092
+ ensureLatestCliVersion: async () => getInstalledCliUpdateRequirement(selfUpdateOptions),
3865
4093
  runtimeConfig,
3866
4094
  settingsStore,
3867
4095
  selfUpdateCli: async () => selfUpdateInstalledCli(selfUpdateOptions),
@@ -3897,6 +4125,13 @@ ${formatOutputModesNote()}`
3897
4125
  }
3898
4126
  this.outputHelp();
3899
4127
  });
4128
+ program2.hook("preAction", async (_command, actionCommand) => {
4129
+ await enforceLatestCliVersionIfNeeded({
4130
+ actionCommand,
4131
+ dependencies,
4132
+ program: program2
4133
+ });
4134
+ });
3900
4135
  const authCommand = configureCommandPresentation(
3901
4136
  new Command2("auth").description("Manage CLI authentication.")
3902
4137
  ).helpGroup("Workspace:").addHelpText(