@tinacms/cli 2.2.5 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,11 +32,31 @@ Commands:
32
32
  schema:types [options] Generate a GraphQL query for your site's schema, (and optionally Typescript types)
33
33
  init [options] Add TinaCloud to an existing project
34
34
  audit [options] Audit your schema and the files to check for errors
35
+ doctor [options] Check direct TinaCMS dependencies against npm latest
35
36
  help [command] display help for command
36
37
  ```
37
38
 
38
39
  [See our docs](https://tina.io/docs/cli-overview/) for more information about the commands.
39
40
 
41
+ ### `doctor`
42
+
43
+ Use `tinacms doctor` to check whether the Tina packages installed in your
44
+ project are current against the npm `latest` dist-tag.
45
+
46
+ The command reads your `package.json`, resolves installed versions from
47
+ `node_modules` or the project lockfile, filters to Tina package names
48
+ (`tinacms`, `@tinacms/*`, `tinacms-*`, `next-tinacms-*`, and
49
+ `create-tina-app`), then prints the installed version next to the latest
50
+ published version.
51
+
52
+ ```bash
53
+ pnpm tinacms doctor
54
+ ```
55
+
56
+ Use `--json` for machine-readable output. The command exits with code `1` if a
57
+ Tina package is outdated or if a package version cannot be checked, which makes
58
+ it suitable for CI.
59
+
40
60
  ## Getting started
41
61
 
42
62
  The simplest way to get started is to add a `.tina/schema.ts` file
package/dist/index.js CHANGED
@@ -2,11 +2,12 @@
2
2
  import { Cli, Builtins } from "clipanion";
3
3
 
4
4
  // package.json
5
- var version = "2.2.5";
5
+ var version = "2.3.0";
6
6
 
7
7
  // src/next/commands/dev-command/index.ts
8
8
  import path9 from "path";
9
9
  import { FilesystemBridge as FilesystemBridge2, buildSchema } from "@tinacms/graphql";
10
+ import { Telemetry } from "@tinacms/metrics";
10
11
  import { LocalSearchIndexClient, SearchIndexer } from "@tinacms/search";
11
12
  import AsyncLock from "async-lock";
12
13
  import chokidar from "chokidar";
@@ -511,14 +512,6 @@ var Codegen = class {
511
512
  );
512
513
  await fs.ensureFile(filePath);
513
514
  await fs.outputFile(filePath, data);
514
- if (this.configManager.hasSeparateContentRoot()) {
515
- const filePath2 = path.join(
516
- this.configManager.generatedFolderPathContentRepo,
517
- fileName
518
- );
519
- await fs.ensureFile(filePath2);
520
- await fs.outputFile(filePath2, data);
521
- }
522
515
  }
523
516
  async removeGeneratedFilesIfExists() {
524
517
  await unlinkIfExists(this.configManager.generatedClientJSFilePath);
@@ -691,7 +684,7 @@ var Codegen = class {
691
684
  import { resolve } from "@tinacms/datalayer";
692
685
  import type { TinaClient } from "tinacms/dist/client";
693
686
 
694
- import { queries } from "./types";
687
+ import { queries } from "${this.configManager.isUsingTs() ? "./types" : "./types.js"}";
695
688
  import database from "../database";
696
689
 
697
690
  export async function databaseRequest({ query, variables, user }) {
@@ -757,7 +750,7 @@ export default databaseClient;
757
750
  const errorPolicy = this.configManager.config?.client?.errorPolicy;
758
751
  const apiURL = this.getApiURL();
759
752
  const clientString = `import { createClient } from "tinacms/dist/client";
760
- import { queries } from "./types";
753
+ import { queries } from "${this.configManager.isUsingTs() ? "./types" : "./types.js"}";
761
754
  export const client = createClient({ ${this.noClientBuildCache === false ? `cacheDir: '${normalizePath(
762
755
  this.configManager.generatedCachePath
763
756
  )}', ` : ""}url: ${this.localContentBuild ? `process.env.TINA_LOCAL_URL || '${apiURL}'` : `'${apiURL}'`}, token: '${token}', queries, ${errorPolicy ? `errorPolicy: '${errorPolicy}'` : ""} });
@@ -826,7 +819,6 @@ import { pathToFileURL } from "url";
826
819
  import * as esbuild from "esbuild";
827
820
  import * as dotenv from "dotenv";
828
821
  import normalizePath2 from "normalize-path";
829
- import chalk4 from "chalk";
830
822
  import { createRequire } from "module";
831
823
 
832
824
  // src/next/resolve-content-root.ts
@@ -894,13 +886,13 @@ var ConfigManager = class {
894
886
  rootPath;
895
887
  tinaFolderPath;
896
888
  isUsingLegacyFolder;
889
+ hasWarnedLegacyFolder = false;
897
890
  tinaConfigFilePath;
898
891
  tinaSpaPackagePath;
899
892
  contentRootPath;
900
893
  envFilePath;
901
894
  generatedCachePath;
902
895
  generatedFolderPath;
903
- generatedFolderPathContentRepo;
904
896
  generatedGraphQLGQLPath;
905
897
  generatedGraphQLJSONPath;
906
898
  generatedSchemaJSONPath;
@@ -957,6 +949,14 @@ var ConfigManager = class {
957
949
  async processConfig() {
958
950
  const require2 = createRequire(import.meta.url);
959
951
  this.tinaFolderPath = await this.getTinaFolderPath(this.rootPath);
952
+ if (this.isUsingLegacyFolder && !this.hasWarnedLegacyFolder) {
953
+ this.hasWarnedLegacyFolder = true;
954
+ logger.warn(
955
+ warnText(
956
+ "WARN: Detected legacy `.tina/` config folder. `tina-lock.json` is only generated for the new `tina/` layout, and TinaCloud requires it to index your schema. Migrate by renaming `.tina/` to `tina/` (the contents stay the same). See https://tina.io/docs/tina-folder/overview/."
957
+ )
958
+ );
959
+ }
960
960
  this.envFilePath = path4.resolve(
961
961
  path4.join(this.tinaFolderPath, "..", ".env")
962
962
  );
@@ -1078,16 +1078,10 @@ var ConfigManager = class {
1078
1078
  tinaConfigFilePath: this.tinaConfigFilePath,
1079
1079
  localContentPath: this.config.localContentPath
1080
1080
  });
1081
- this.generatedFolderPathContentRepo = path4.join(
1082
- await this.getTinaFolderPath(this.contentRootPath, {
1083
- isContentRoot: this.hasSeparateContentRoot()
1084
- }),
1085
- GENERATED_FOLDER
1086
- );
1087
1081
  this.spaMainPath = require2.resolve("@tinacms/app");
1088
1082
  this.spaRootPath = path4.join(this.spaMainPath, "..", "..");
1089
1083
  }
1090
- async getTinaFolderPath(rootPath, { isContentRoot } = {}) {
1084
+ async getTinaFolderPath(rootPath) {
1091
1085
  const tinaFolderPath = path4.join(rootPath, TINA_FOLDER);
1092
1086
  const tinaFolderExists = await fs3.pathExists(tinaFolderPath);
1093
1087
  if (tinaFolderExists) {
@@ -1100,11 +1094,6 @@ var ConfigManager = class {
1100
1094
  this.isUsingLegacyFolder = true;
1101
1095
  return legacyFolderPath;
1102
1096
  }
1103
- if (isContentRoot) {
1104
- throw new Error(
1105
- `Unable to find a ${chalk4.cyan("tina/")} folder in your content root at ${chalk4.cyan(rootPath)}. When using localContentPath, the content directory must contain a ${chalk4.cyan("tina/")} folder for generated files. Create one with: mkdir ${path4.join(rootPath, TINA_FOLDER)}`
1106
- );
1107
- }
1108
1097
  throw new Error(
1109
1098
  `Unable to find Tina folder, if you're working in folder outside of the Tina config be sure to specify --rootPath`
1110
1099
  );
@@ -1129,28 +1118,41 @@ var ConfigManager = class {
1129
1118
  }
1130
1119
  printGeneratedClientFilePath() {
1131
1120
  if (this.isUsingTs()) {
1132
- return this.generatedClientTSFilePath.replace(`${this.rootPath}/`, "");
1121
+ return normalizePath2(
1122
+ path4.relative(this.rootPath, this.generatedClientTSFilePath)
1123
+ );
1133
1124
  }
1134
- return this.generatedClientJSFilePath.replace(`${this.rootPath}/`, "");
1125
+ return normalizePath2(
1126
+ path4.relative(this.rootPath, this.generatedClientJSFilePath)
1127
+ );
1135
1128
  }
1136
1129
  printGeneratedTypesFilePath() {
1137
- return this.generatedTypesTSFilePath.replace(`${this.rootPath}/`, "");
1130
+ return normalizePath2(
1131
+ path4.relative(this.rootPath, this.generatedTypesTSFilePath)
1132
+ );
1138
1133
  }
1139
1134
  printoutputHTMLFilePath() {
1140
- return this.outputHTMLFilePath.replace(`${this.publicFolderPath}/`, "");
1135
+ return normalizePath2(
1136
+ path4.relative(this.publicFolderPath, this.outputHTMLFilePath)
1137
+ );
1141
1138
  }
1142
1139
  printRelativePath(filename) {
1143
1140
  if (filename) {
1144
- return filename.replace(/\\/g, "/").replace(`${this.rootPath}/`, "");
1141
+ return normalizePath2(path4.relative(this.rootPath, filename));
1145
1142
  }
1146
1143
  throw `No path provided to print`;
1147
1144
  }
1148
1145
  printPrebuildFilePath() {
1149
- return this.prebuildFilePath.replace(/\\/g, "/").replace(`${this.rootPath}/${this.tinaFolderPath}/`, "");
1146
+ return normalizePath2(
1147
+ path4.relative(
1148
+ path4.join(this.rootPath, this.tinaFolderPath),
1149
+ this.prebuildFilePath
1150
+ )
1151
+ );
1150
1152
  }
1151
1153
  printContentRelativePath(filename) {
1152
1154
  if (filename) {
1153
- return filename.replace(/\\/g, "/").replace(`${this.contentRootPath}/`, "");
1155
+ return normalizePath2(path4.relative(this.contentRootPath, filename));
1154
1156
  }
1155
1157
  throw `No path provided to print`;
1156
1158
  }
@@ -1351,7 +1353,7 @@ async function createAndInitializeDatabase(configManager, datalayerPort, bridgeO
1351
1353
 
1352
1354
  // src/next/commands/baseCommands.ts
1353
1355
  import { Command, Option } from "clipanion";
1354
- import chalk5 from "chalk";
1356
+ import chalk4 from "chalk";
1355
1357
 
1356
1358
  // src/utils/start-subprocess.ts
1357
1359
  import childProcess from "child_process";
@@ -1419,7 +1421,7 @@ var BaseCommand = class extends Command {
1419
1421
  if (this.subCommand) {
1420
1422
  subProc = await startSubprocess2({ command: this.subCommand });
1421
1423
  logger.info(
1422
- `Running web application with command: ${chalk5.cyan(this.subCommand)}`
1424
+ `Running web application with command: ${chalk4.cyan(this.subCommand)}`
1423
1425
  );
1424
1426
  }
1425
1427
  function exitHandler(options, exitCode) {
@@ -1499,7 +1501,7 @@ var BaseCommand = class extends Command {
1499
1501
  pathFilter
1500
1502
  });
1501
1503
  const tinaPathUpdates = modified.filter(
1502
- (path16) => path16.startsWith(".tina/__generated__/_schema.json") || path16.startsWith("tina/tina-lock.json")
1504
+ (path17) => path17.startsWith(".tina/__generated__/_schema.json") || path17.startsWith("tina/tina-lock.json")
1503
1505
  );
1504
1506
  if (tinaPathUpdates.length > 0) {
1505
1507
  res = await database.indexContent({
@@ -2730,6 +2732,15 @@ var DevCommand = class extends BaseCommand {
2730
2732
  try {
2731
2733
  await configManager.processConfig();
2732
2734
  if (firstTime) {
2735
+ const telemetry = new Telemetry({ disabled: this.noTelemetry });
2736
+ await telemetry.submitRecord({
2737
+ event: {
2738
+ name: "tinacms:cli:dev:invoke",
2739
+ hasLocalContentPath: Boolean(
2740
+ configManager.config.localContentPath
2741
+ )
2742
+ }
2743
+ });
2733
2744
  database = await createAndInitializeDatabase(
2734
2745
  configManager,
2735
2746
  Number(this.datalayerPort)
@@ -2770,15 +2781,6 @@ var DevCommand = class extends BaseCommand {
2770
2781
  path9.join(configManager.tinaFolderPath, tinaLockFilename),
2771
2782
  tinaLockContent
2772
2783
  );
2773
- if (configManager.hasSeparateContentRoot()) {
2774
- const rootPath = await configManager.getTinaFolderPath(
2775
- configManager.contentRootPath,
2776
- { isContentRoot: true }
2777
- );
2778
- const filePath = path9.join(rootPath, tinaLockFilename);
2779
- await fs8.ensureFile(filePath);
2780
- await fs8.outputFile(filePath, tinaLockContent);
2781
- }
2782
2784
  }
2783
2785
  await this.indexContentWithSpinner({
2784
2786
  database,
@@ -3019,6 +3021,7 @@ import crypto from "crypto";
3019
3021
  import path10 from "path";
3020
3022
  import { diff } from "@graphql-inspector/core";
3021
3023
  import { FilesystemBridge as FilesystemBridge3, buildSchema as buildSchema2 } from "@tinacms/graphql";
3024
+ import { Telemetry as Telemetry2 } from "@tinacms/metrics";
3022
3025
  import { parseURL as parseURL2 } from "@tinacms/schema-tools";
3023
3026
  import {
3024
3027
  SearchIndexer as SearchIndexer2,
@@ -3252,6 +3255,13 @@ ${dangerText(e.message)}`);
3252
3255
  );
3253
3256
  process.exit(1);
3254
3257
  }
3258
+ const telemetry = new Telemetry2({ disabled: this.noTelemetry });
3259
+ await telemetry.submitRecord({
3260
+ event: {
3261
+ name: "tinacms:cli:build:invoke",
3262
+ hasLocalContentPath: Boolean(configManager.config.localContentPath)
3263
+ }
3264
+ });
3255
3265
  if (localContentOnly && !this.localOption) {
3256
3266
  const config2 = configManager.config;
3257
3267
  const missing = [];
@@ -3882,9 +3892,9 @@ import { buildSchema as buildSchema3 } from "@tinacms/graphql";
3882
3892
 
3883
3893
  // src/next/commands/audit-command/audit.ts
3884
3894
  import prompts from "prompts";
3885
- import { Telemetry } from "@tinacms/metrics";
3895
+ import { Telemetry as Telemetry3 } from "@tinacms/metrics";
3886
3896
  import { resolve } from "@tinacms/graphql";
3887
- import chalk6 from "chalk";
3897
+ import chalk5 from "chalk";
3888
3898
  var audit = async ({
3889
3899
  database,
3890
3900
  clean,
@@ -3892,7 +3902,7 @@ var audit = async ({
3892
3902
  noTelemetry,
3893
3903
  verbose
3894
3904
  }) => {
3895
- const telemetry = new Telemetry({ disabled: noTelemetry });
3905
+ const telemetry = new Telemetry3({ disabled: noTelemetry });
3896
3906
  await telemetry.submitRecord({
3897
3907
  event: {
3898
3908
  name: "tinacms:cli:audit:invoke",
@@ -3902,7 +3912,7 @@ var audit = async ({
3902
3912
  });
3903
3913
  if (clean) {
3904
3914
  logger.info(
3905
- `You are using the \`--clean\` option. This will modify your content as if a user is submitting a form. Before running this you should have a ${chalk6.bold(
3915
+ `You are using the \`--clean\` option. This will modify your content as if a user is submitting a form. Before running this you should have a ${chalk5.bold(
3906
3916
  "clean git tree"
3907
3917
  )} so unwanted changes can be undone.
3908
3918
 
@@ -3914,13 +3924,13 @@ var audit = async ({
3914
3924
  message: `Do you want to continue?`
3915
3925
  });
3916
3926
  if (!res.useClean) {
3917
- logger.warn(chalk6.yellowBright("\u26A0\uFE0F Audit not complete"));
3927
+ logger.warn(chalk5.yellowBright("\u26A0\uFE0F Audit not complete"));
3918
3928
  process.exit(0);
3919
3929
  }
3920
3930
  }
3921
3931
  if (useDefaultValues && !clean) {
3922
3932
  logger.warn(
3923
- chalk6.yellowBright(
3933
+ chalk5.yellowBright(
3924
3934
  "WARNING: using the `--useDefaultValues` without the `--clean` flag has no effect. Please re-run audit and add the `--clean` flag"
3925
3935
  )
3926
3936
  );
@@ -3948,10 +3958,10 @@ var audit = async ({
3948
3958
  }
3949
3959
  if (error) {
3950
3960
  logger.error(
3951
- chalk6.redBright(`\u203C\uFE0F Audit ${chalk6.bold("failed")} with errors`)
3961
+ chalk5.redBright(`\u203C\uFE0F Audit ${chalk5.bold("failed")} with errors`)
3952
3962
  );
3953
3963
  } else {
3954
- logger.info(chalk6.greenBright("\u2705 Audit passed"));
3964
+ logger.info(chalk5.greenBright("\u2705 Audit passed"));
3955
3965
  }
3956
3966
  };
3957
3967
  var auditDocuments = async (args) => {
@@ -3979,11 +3989,11 @@ var auditDocuments = async (args) => {
3979
3989
  if (docResult.errors) {
3980
3990
  error = true;
3981
3991
  docResult.errors.forEach((err) => {
3982
- logger.error(chalk6.red(err.message));
3992
+ logger.error(chalk5.red(err.message));
3983
3993
  if (err.originalError.originalError) {
3984
3994
  logger.error(
3985
3995
  // @ts-ignore FIXME: this doesn't seem right
3986
- chalk6.red(` ${err.originalError.originalError.message}`)
3996
+ chalk5.red(` ${err.originalError.originalError.message}`)
3987
3997
  );
3988
3998
  }
3989
3999
  });
@@ -4025,7 +4035,7 @@ var auditDocuments = async (args) => {
4025
4035
  if (mutationRes.errors) {
4026
4036
  mutationRes.errors.forEach((err) => {
4027
4037
  error = true;
4028
- logger.error(chalk6.red(err.message));
4038
+ logger.error(chalk5.red(err.message));
4029
4039
  });
4030
4040
  }
4031
4041
  }
@@ -5650,7 +5660,7 @@ var rewriteTemplateKeysInDocs = (args) => {
5650
5660
  };
5651
5661
 
5652
5662
  // src/cmds/init/apply.ts
5653
- import { Telemetry as Telemetry2 } from "@tinacms/metrics";
5663
+ import { Telemetry as Telemetry4 } from "@tinacms/metrics";
5654
5664
  import fs15 from "fs-extra";
5655
5665
 
5656
5666
  // src/next/commands/codemod-command/index.ts
@@ -6613,7 +6623,7 @@ var reportTelemetry = async ({
6613
6623
  if (noTelemetry) {
6614
6624
  logger.info(logText("Telemetry disabled"));
6615
6625
  }
6616
- const telemetry = new Telemetry2({ disabled: noTelemetry });
6626
+ const telemetry = new Telemetry4({ disabled: noTelemetry });
6617
6627
  const schemaFileType = usingTypescript ? "ts" : "js";
6618
6628
  await telemetry.submitRecord({
6619
6629
  event: {
@@ -6711,22 +6721,22 @@ var writeGeneratedFile = async ({
6711
6721
  content,
6712
6722
  typescript
6713
6723
  }) => {
6714
- const { exists, path: path16, parentPath } = generatedFile.resolve(typescript);
6724
+ const { exists, path: path17, parentPath } = generatedFile.resolve(typescript);
6715
6725
  if (exists) {
6716
6726
  if (overwrite) {
6717
- logger.info(`Overwriting file at ${path16}... \u2705`);
6718
- fs15.outputFileSync(path16, content);
6727
+ logger.info(`Overwriting file at ${path17}... \u2705`);
6728
+ fs15.outputFileSync(path17, content);
6719
6729
  } else {
6720
- logger.info(`Not overwriting file at ${path16}.`);
6730
+ logger.info(`Not overwriting file at ${path17}.`);
6721
6731
  logger.info(
6722
- logText(`Please add the following to ${path16}:
6732
+ logText(`Please add the following to ${path17}:
6723
6733
  ${indentText(content)}}`)
6724
6734
  );
6725
6735
  }
6726
6736
  } else {
6727
- logger.info(`Adding file at ${path16}... \u2705`);
6737
+ logger.info(`Adding file at ${path17}... \u2705`);
6728
6738
  await fs15.ensureDir(parentPath);
6729
- fs15.outputFileSync(path16, content);
6739
+ fs15.outputFileSync(path17, content);
6730
6740
  }
6731
6741
  };
6732
6742
  var addConfigFile = async ({
@@ -7121,6 +7131,356 @@ var SearchIndexCommand = class extends Command7 {
7121
7131
  }
7122
7132
  };
7123
7133
 
7134
+ // src/next/commands/doctor-command/index.ts
7135
+ import { Command as Command8, Option as Option8 } from "clipanion";
7136
+
7137
+ // src/next/commands/doctor-command/doctor.ts
7138
+ import path16 from "path";
7139
+ import fs16 from "fs-extra";
7140
+ import yaml3 from "js-yaml";
7141
+ var DEPENDENCY_TYPES = [
7142
+ "dependencies",
7143
+ "devDependencies",
7144
+ "optionalDependencies",
7145
+ "peerDependencies"
7146
+ ];
7147
+ var LOCAL_REFERENCE_PREFIXES = [
7148
+ "file:",
7149
+ "link:",
7150
+ "patch:",
7151
+ "portal:",
7152
+ "workspace:"
7153
+ ];
7154
+ function isTinaPackage(name2) {
7155
+ return name2 === "tinacms" || name2 === "create-tina-app" || name2.startsWith("@tinacms/") || name2.startsWith("tinacms-") || name2.startsWith("next-tinacms-");
7156
+ }
7157
+ function getTinaDependencies(packageJson) {
7158
+ const dependencies = /* @__PURE__ */ new Map();
7159
+ for (const dependencyType of DEPENDENCY_TYPES) {
7160
+ for (const [name2, declared] of Object.entries(
7161
+ packageJson[dependencyType] || {}
7162
+ )) {
7163
+ if (!isTinaPackage(name2) || dependencies.has(name2)) continue;
7164
+ dependencies.set(name2, { name: name2, declared, dependencyType });
7165
+ }
7166
+ }
7167
+ return [...dependencies.values()].sort(
7168
+ (a, b) => a.name.localeCompare(b.name)
7169
+ );
7170
+ }
7171
+ async function readProjectPackageJson(rootPath) {
7172
+ const packageJsonPath = path16.join(rootPath, "package.json");
7173
+ if (!await fs16.pathExists(packageJsonPath)) {
7174
+ throw new Error(`No package.json found at ${packageJsonPath}`);
7175
+ }
7176
+ return fs16.readJSON(packageJsonPath);
7177
+ }
7178
+ async function resolveInstalledVersions({
7179
+ rootPath,
7180
+ dependencies
7181
+ }) {
7182
+ const lockfileVersions = await readLockfileVersions(rootPath);
7183
+ return Promise.all(
7184
+ dependencies.map(async (dependency) => {
7185
+ const installed = await readNodeModulesVersion(rootPath, dependency.name) || lockfileVersions.get(dependency.name);
7186
+ return { ...dependency, installed };
7187
+ })
7188
+ );
7189
+ }
7190
+ async function fetchLatestVersion(packageName, timeoutMs) {
7191
+ const controller = new AbortController();
7192
+ const timeout2 = setTimeout(() => controller.abort(), timeoutMs);
7193
+ try {
7194
+ const response = await fetch(
7195
+ `https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
7196
+ { signal: controller.signal }
7197
+ );
7198
+ if (!response.ok) {
7199
+ throw new Error(`npm registry returned ${response.status}`);
7200
+ }
7201
+ const body = await response.json();
7202
+ const latest = body?.["dist-tags"]?.latest;
7203
+ if (typeof latest !== "string" || latest.length === 0) {
7204
+ throw new Error("npm registry response did not include dist-tags.latest");
7205
+ }
7206
+ return latest;
7207
+ } finally {
7208
+ clearTimeout(timeout2);
7209
+ }
7210
+ }
7211
+ function classifyDependency(dependency, latest, error) {
7212
+ if (!dependency.installed) {
7213
+ return {
7214
+ ...dependency,
7215
+ latest,
7216
+ status: "unknown",
7217
+ error: "Installed version could not be resolved from node_modules or lockfile"
7218
+ };
7219
+ }
7220
+ if (isLocalReference(dependency.installed)) {
7221
+ return {
7222
+ ...dependency,
7223
+ latest,
7224
+ status: "local",
7225
+ error
7226
+ };
7227
+ }
7228
+ if (!latest || error) {
7229
+ return {
7230
+ ...dependency,
7231
+ latest,
7232
+ status: "unknown",
7233
+ error
7234
+ };
7235
+ }
7236
+ return {
7237
+ ...dependency,
7238
+ latest,
7239
+ status: normalizeVersion(dependency.installed) === normalizeVersion(latest) ? "current" : "outdated"
7240
+ };
7241
+ }
7242
+ async function checkTinaDependencies({
7243
+ dependencies,
7244
+ fetchLatest
7245
+ }) {
7246
+ return Promise.all(
7247
+ dependencies.map(async (dependency) => {
7248
+ try {
7249
+ const latest = await fetchLatest(dependency.name);
7250
+ return classifyDependency(dependency, latest);
7251
+ } catch (error) {
7252
+ return classifyDependency(
7253
+ dependency,
7254
+ void 0,
7255
+ error instanceof Error ? error.message : String(error)
7256
+ );
7257
+ }
7258
+ })
7259
+ );
7260
+ }
7261
+ function formatDoctorTable(results) {
7262
+ const rows = [
7263
+ ["Package", "Type", "Declared", "Installed", "Latest", "Status"],
7264
+ ...results.map((result) => [
7265
+ result.name,
7266
+ result.dependencyType,
7267
+ result.declared,
7268
+ result.installed || "-",
7269
+ result.latest || "-",
7270
+ formatStatus(result)
7271
+ ])
7272
+ ];
7273
+ const widths = rows[0].map(
7274
+ (_, column) => Math.max(...rows.map((row) => row[column].length))
7275
+ );
7276
+ return rows.map((row, rowIndex) => {
7277
+ const line = row.map((cell, column) => cell.padEnd(widths[column])).join(" ");
7278
+ return rowIndex === 0 ? `${line}
7279
+ ${widths.map((w) => "-".repeat(w)).join(" ")}` : line;
7280
+ }).join("\n");
7281
+ }
7282
+ function formatStatus(result) {
7283
+ if (result.status === "outdated") return "OUTDATED";
7284
+ if (result.status === "local") return "LOCAL";
7285
+ if (result.status === "unknown")
7286
+ return `UNKNOWN${result.error ? ` (${result.error})` : ""}`;
7287
+ return "CURRENT";
7288
+ }
7289
+ function normalizeVersion(version2) {
7290
+ return version2.trim().replace(/^v/, "").split("(")[0].trim();
7291
+ }
7292
+ function isLocalReference(version2) {
7293
+ const normalized = version2.trim();
7294
+ return LOCAL_REFERENCE_PREFIXES.some(
7295
+ (prefix) => normalized.startsWith(prefix)
7296
+ );
7297
+ }
7298
+ async function readNodeModulesVersion(rootPath, packageName) {
7299
+ const packageJsonPath = path16.join(
7300
+ rootPath,
7301
+ "node_modules",
7302
+ ...packageName.split("/"),
7303
+ "package.json"
7304
+ );
7305
+ if (!await fs16.pathExists(packageJsonPath)) return void 0;
7306
+ const packageJson = await fs16.readJSON(packageJsonPath);
7307
+ return typeof packageJson.version === "string" ? packageJson.version : void 0;
7308
+ }
7309
+ async function readLockfileVersions(rootPath) {
7310
+ const readers = [
7311
+ readPackageLockVersions,
7312
+ readPnpmLockVersions,
7313
+ readYarnLockVersions
7314
+ ];
7315
+ for (const reader of readers) {
7316
+ const versions = await reader(rootPath);
7317
+ if (versions.size > 0) return versions;
7318
+ }
7319
+ return /* @__PURE__ */ new Map();
7320
+ }
7321
+ async function readPackageLockVersions(rootPath) {
7322
+ const lockfilePath = path16.join(rootPath, "package-lock.json");
7323
+ const versions = /* @__PURE__ */ new Map();
7324
+ if (!await fs16.pathExists(lockfilePath)) return versions;
7325
+ const lockfile = await fs16.readJSON(lockfilePath);
7326
+ for (const [key, value] of Object.entries(lockfile.packages || {})) {
7327
+ if (!key.startsWith("node_modules/")) continue;
7328
+ const name2 = key.replace(/^node_modules\//, "");
7329
+ const version2 = value.version;
7330
+ if (isTinaPackage(name2) && typeof version2 === "string") {
7331
+ versions.set(name2, version2);
7332
+ }
7333
+ }
7334
+ for (const [name2, value] of Object.entries(lockfile.dependencies || {})) {
7335
+ const version2 = value.version;
7336
+ if (isTinaPackage(name2) && typeof version2 === "string") {
7337
+ versions.set(name2, version2);
7338
+ }
7339
+ }
7340
+ return versions;
7341
+ }
7342
+ async function readPnpmLockVersions(rootPath) {
7343
+ const lockfilePath = path16.join(rootPath, "pnpm-lock.yaml");
7344
+ const versions = /* @__PURE__ */ new Map();
7345
+ if (!await fs16.pathExists(lockfilePath)) return versions;
7346
+ const lockfile = yaml3.load(await fs16.readFile(lockfilePath, "utf8"));
7347
+ const rootImporter = lockfile?.importers?.["."];
7348
+ for (const dependencyType of DEPENDENCY_TYPES) {
7349
+ for (const [name2, value] of Object.entries(
7350
+ rootImporter?.[dependencyType] || {}
7351
+ )) {
7352
+ if (!isTinaPackage(name2)) continue;
7353
+ const version2 = typeof value === "string" ? value : typeof value.version === "string" ? value.version : void 0;
7354
+ if (version2) versions.set(name2, normalizeInstalledVersion(version2));
7355
+ }
7356
+ }
7357
+ for (const key of Object.keys(lockfile?.packages || {})) {
7358
+ const parsed = parsePnpmPackageKey(key);
7359
+ if (parsed && isTinaPackage(parsed.name) && !versions.has(parsed.name)) {
7360
+ versions.set(parsed.name, parsed.version);
7361
+ }
7362
+ }
7363
+ return versions;
7364
+ }
7365
+ async function readYarnLockVersions(rootPath) {
7366
+ const lockfilePath = path16.join(rootPath, "yarn.lock");
7367
+ const versions = /* @__PURE__ */ new Map();
7368
+ if (!await fs16.pathExists(lockfilePath)) return versions;
7369
+ const contents = await fs16.readFile(lockfilePath, "utf8");
7370
+ if (contents.includes("__metadata:")) {
7371
+ return readYarnBerryLockVersions(contents);
7372
+ }
7373
+ const lines = contents.split("\n");
7374
+ let activeNames = [];
7375
+ for (const line of lines) {
7376
+ if (line.length > 0 && !line.startsWith(" ") && line.includes("@")) {
7377
+ activeNames = extractYarnPackageNames(line);
7378
+ continue;
7379
+ }
7380
+ const version2 = line.match(/^\s+version\s+"([^"]+)"/)?.[1];
7381
+ if (!version2) continue;
7382
+ for (const name2 of activeNames) {
7383
+ if (isTinaPackage(name2) && !versions.has(name2)) {
7384
+ versions.set(name2, version2);
7385
+ }
7386
+ }
7387
+ }
7388
+ return versions;
7389
+ }
7390
+ function readYarnBerryLockVersions(contents) {
7391
+ const versions = /* @__PURE__ */ new Map();
7392
+ const lockfile = yaml3.load(contents);
7393
+ for (const [descriptor, value] of Object.entries(lockfile || {})) {
7394
+ if (descriptor === "__metadata") continue;
7395
+ const name2 = extractYarnBerryPackageName(descriptor);
7396
+ const version2 = getYarnBerryInstalledVersion(descriptor, value?.version);
7397
+ if (name2 && isTinaPackage(name2) && version2) {
7398
+ versions.set(name2, version2);
7399
+ }
7400
+ }
7401
+ return versions;
7402
+ }
7403
+ function parsePnpmPackageKey(key) {
7404
+ const normalized = key.replace(/^\//, "");
7405
+ const match = normalized.match(/^(@[^/]+\/[^@]+|[^@/]+)@([^(/]+)/);
7406
+ if (!match) return void 0;
7407
+ return { name: match[1], version: normalizeVersion(match[2]) };
7408
+ }
7409
+ function normalizeInstalledVersion(version2) {
7410
+ return isLocalReference(version2) ? version2 : normalizeVersion(version2);
7411
+ }
7412
+ function extractYarnPackageNames(line) {
7413
+ return line.replace(/:$/, "").split(/,\s*/).map((descriptor) => descriptor.trim().replace(/^"|"$/g, "")).map((descriptor) => {
7414
+ if (descriptor.startsWith("@")) {
7415
+ const [, scope, name2] = descriptor.match(/^(@[^/]+)\/([^@]+)/) || [];
7416
+ return scope && name2 ? `${scope}/${name2}` : descriptor;
7417
+ }
7418
+ return descriptor.split("@")[0];
7419
+ }).filter(Boolean);
7420
+ }
7421
+ function extractYarnBerryPackageName(descriptor) {
7422
+ if (descriptor.startsWith("@")) {
7423
+ return descriptor.match(/^(@[^/]+\/[^@]+)/)?.[1];
7424
+ }
7425
+ return descriptor.split("@")[0] || void 0;
7426
+ }
7427
+ function getYarnBerryInstalledVersion(descriptor, version2) {
7428
+ const name2 = extractYarnBerryPackageName(descriptor);
7429
+ const reference = name2 ? descriptor.slice(name2.length + 1) : void 0;
7430
+ if (reference && isLocalReference(reference)) return reference;
7431
+ return typeof version2 === "string" ? normalizeInstalledVersion(version2) : void 0;
7432
+ }
7433
+
7434
+ // src/next/commands/doctor-command/index.ts
7435
+ var DoctorCommand = class extends Command8 {
7436
+ static paths = [["doctor"]];
7437
+ rootPath = Option8.String("--rootPath", {
7438
+ description: "Specify the root directory to inspect (defaults to current working directory)"
7439
+ });
7440
+ json = Option8.Boolean("--json", false, {
7441
+ description: "Print machine-readable JSON output"
7442
+ });
7443
+ timeout = Option8.String("--timeout", "5000", {
7444
+ description: "npm registry request timeout in milliseconds"
7445
+ });
7446
+ static usage = Command8.Usage({
7447
+ category: "Commands",
7448
+ description: "Check direct TinaCMS dependencies against npm latest"
7449
+ });
7450
+ async catch(error) {
7451
+ const message = error instanceof Error ? error.message : String(error);
7452
+ logger.error(`Error occurred during tinacms doctor: ${message}`);
7453
+ process.exit(1);
7454
+ }
7455
+ async execute() {
7456
+ const rootPath = this.rootPath || process.cwd();
7457
+ const timeoutMs = Number(this.timeout);
7458
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
7459
+ logger.error("--timeout must be a positive number");
7460
+ return 1;
7461
+ }
7462
+ const packageJson = await readProjectPackageJson(rootPath);
7463
+ const dependencies = await resolveInstalledVersions({
7464
+ rootPath,
7465
+ dependencies: getTinaDependencies(packageJson)
7466
+ });
7467
+ const results = await checkTinaDependencies({
7468
+ dependencies,
7469
+ fetchLatest: (name2) => fetchLatestVersion(name2, timeoutMs)
7470
+ });
7471
+ if (this.json) {
7472
+ logger.info(JSON.stringify({ rootPath, results }, null, 2));
7473
+ } else if (results.length === 0) {
7474
+ logger.info("No direct TinaCMS dependencies found.");
7475
+ } else {
7476
+ logger.info(formatDoctorTable(results));
7477
+ }
7478
+ const hasOutdated = results.some((result) => result.status === "outdated");
7479
+ const hasUnknown = results.some((result) => result.status === "unknown");
7480
+ return hasOutdated || hasUnknown ? 1 : 0;
7481
+ }
7482
+ };
7483
+
7124
7484
  // src/index.ts
7125
7485
  var cli = new Cli({
7126
7486
  binaryName: `tinacms`,
@@ -7133,6 +7493,7 @@ cli.register(AuditCommand);
7133
7493
  cli.register(InitCommand);
7134
7494
  cli.register(CodemodCommand);
7135
7495
  cli.register(SearchIndexCommand);
7496
+ cli.register(DoctorCommand);
7136
7497
  cli.register(Builtins.DefinitionsCommand);
7137
7498
  cli.register(Builtins.HelpCommand);
7138
7499
  cli.register(Builtins.VersionCommand);
@@ -0,0 +1,34 @@
1
+ export type DoctorStatus = 'current' | 'local' | 'outdated' | 'unknown';
2
+ export type TinaDependency = {
3
+ name: string;
4
+ declared: string;
5
+ dependencyType: DependencyType;
6
+ installed?: string;
7
+ };
8
+ export type DoctorResult = TinaDependency & {
9
+ latest?: string;
10
+ status: DoctorStatus;
11
+ error?: string;
12
+ };
13
+ type PackageJson = {
14
+ dependencies?: Record<string, string>;
15
+ devDependencies?: Record<string, string>;
16
+ optionalDependencies?: Record<string, string>;
17
+ peerDependencies?: Record<string, string>;
18
+ };
19
+ type DependencyType = 'dependencies' | 'devDependencies' | 'optionalDependencies' | 'peerDependencies';
20
+ export declare function isTinaPackage(name: string): boolean;
21
+ export declare function getTinaDependencies(packageJson: PackageJson): TinaDependency[];
22
+ export declare function readProjectPackageJson(rootPath: string): Promise<any>;
23
+ export declare function resolveInstalledVersions({ rootPath, dependencies, }: {
24
+ rootPath: string;
25
+ dependencies: TinaDependency[];
26
+ }): Promise<TinaDependency[]>;
27
+ export declare function fetchLatestVersion(packageName: string, timeoutMs: number): Promise<string>;
28
+ export declare function classifyDependency(dependency: TinaDependency, latest?: string, error?: string): DoctorResult;
29
+ export declare function checkTinaDependencies({ dependencies, fetchLatest, }: {
30
+ dependencies: TinaDependency[];
31
+ fetchLatest: (name: string) => Promise<string>;
32
+ }): Promise<DoctorResult[]>;
33
+ export declare function formatDoctorTable(results: DoctorResult[]): string;
34
+ export {};
@@ -0,0 +1,10 @@
1
+ import { Command } from 'clipanion';
2
+ export declare class DoctorCommand extends Command {
3
+ static paths: string[][];
4
+ rootPath: string;
5
+ json: boolean;
6
+ timeout: string;
7
+ static usage: import("clipanion").Usage;
8
+ catch(error: unknown): Promise<void>;
9
+ execute(): Promise<number | void>;
10
+ }
@@ -8,13 +8,13 @@ export declare class ConfigManager {
8
8
  rootPath: string;
9
9
  tinaFolderPath: string;
10
10
  isUsingLegacyFolder: boolean;
11
+ private hasWarnedLegacyFolder;
11
12
  tinaConfigFilePath: string;
12
13
  tinaSpaPackagePath: string;
13
14
  contentRootPath?: string;
14
15
  envFilePath: string;
15
16
  generatedCachePath: string;
16
17
  generatedFolderPath: string;
17
- generatedFolderPathContentRepo: string;
18
18
  generatedGraphQLGQLPath: string;
19
19
  generatedGraphQLJSONPath: string;
20
20
  generatedSchemaJSONPath: string;
@@ -54,21 +54,19 @@ export declare class ConfigManager {
54
54
  hasSeparateContentRoot(): boolean;
55
55
  shouldSkipSDK(): boolean;
56
56
  processConfig(): Promise<void>;
57
- getTinaFolderPath(rootPath: string, { isContentRoot }?: {
58
- isContentRoot?: boolean;
59
- }): Promise<string>;
57
+ getTinaFolderPath(rootPath: string): Promise<string>;
60
58
  getTinaGraphQLVersion(): {
61
59
  fullVersion: string;
62
60
  major: string;
63
61
  minor: string;
64
62
  patch: string;
65
63
  };
66
- printGeneratedClientFilePath(): string;
67
- printGeneratedTypesFilePath(): string;
68
- printoutputHTMLFilePath(): string;
69
- printRelativePath(filename: string): string;
70
- printPrebuildFilePath(): string;
71
- printContentRelativePath(filename: string): string;
64
+ printGeneratedClientFilePath(): any;
65
+ printGeneratedTypesFilePath(): any;
66
+ printoutputHTMLFilePath(): any;
67
+ printRelativePath(filename: string): any;
68
+ printPrebuildFilePath(): any;
69
+ printContentRelativePath(filename: string): any;
72
70
  /**
73
71
  * Given a filepath without an extension, find the first match (eg. tsx, ts, jsx, js)
74
72
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tinacms/cli",
3
3
  "type": "module",
4
- "version": "2.2.5",
4
+ "version": "2.3.0",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
7
7
  "files": [
@@ -41,7 +41,7 @@
41
41
  "@types/progress": "^2.0.7",
42
42
  "@types/prompts": "^2.4.9",
43
43
  "jest": "^29.7.0",
44
- "@tinacms/scripts": "1.6.0"
44
+ "@tinacms/scripts": "1.6.1"
45
45
  },
46
46
  "dependencies": {
47
47
  "@graphql-codegen/core": "^2.6.8",
@@ -88,12 +88,12 @@
88
88
  "vite": "^4.5.9",
89
89
  "yup": "^1.6.1",
90
90
  "zod": "^3.24.2",
91
- "@tinacms/app": "2.4.5",
92
- "@tinacms/graphql": "2.3.0",
93
- "@tinacms/schema-tools": "2.7.3",
94
- "@tinacms/metrics": "2.0.1",
95
- "@tinacms/search": "1.2.12",
96
- "tinacms": "3.7.5"
91
+ "@tinacms/graphql": "2.4.0",
92
+ "@tinacms/schema-tools": "2.7.4",
93
+ "@tinacms/metrics": "2.1.0",
94
+ "@tinacms/search": "1.2.14",
95
+ "@tinacms/app": "2.4.7",
96
+ "tinacms": "3.8.0"
97
97
  },
98
98
  "publishConfig": {
99
99
  "registry": "https://registry.npmjs.org"