run method

  1. @override
Future<int> run()
override

Runs this command.

The return value is wrapped in a Future if necessary and returned by CommandRunner.runCommand.

Implementation

@override
Future<int> run() async {
  rejectRest(super.argResults!, usage);

  final pubspec = Pubspec(currentDirPath: projectPath);
  final content = await pubspec.getContent();
  final scripts = content[scriptsKey];

  final String target;
  if (!content.containsKey(scriptsKey)) {
    target = defaultScriptsFileName;
  } else if (scripts is String) {
    target = scripts;
  } else if (scripts is Map) {
    stdout.writeln('merry is already configured with inline scripts in $pubspecFileName. Nothing changed.');
    return 0;
  } else {
    // A `scripts:` key with no value lands here too — the config is
    // half-written and only the author can say what it should point at.
    throw MerryError(type: ErrorCode.invalidScripts);
  }

  // Mirror the containment check `Pubspec.getScripts` applies when reading, so
  // a `scripts: ../../elsewhere.yaml` cannot make init write outside the project.
  final scriptsPath = path.normalize(path.join(projectPath, target));
  if (!path.isWithin(projectPath, scriptsPath)) {
    throw MerryError(type: ErrorCode.invalidScripts);
  }

  // A directory (or anything else that is not a plain file) cannot be written
  // to; refuse it here instead of leaking a raw FileSystemException.
  final existingType = FileSystemEntity.typeSync(scriptsPath);
  if (existingType != FileSystemEntityType.notFound && existingType != FileSystemEntityType.file) {
    throw MerryError(type: ErrorCode.invalidScripts);
  }

  // The directories leading to the target have to be creatable. An existing
  // non-directory in the way — `scripts: tool/scripts.yaml` where `tool` is a
  // file, or a symlink to a directory that does not exist — would otherwise
  // surface as a raw FileSystemException from the `create(recursive: true)`
  // below. `typeSync` follows links, so a dangling one reads as `notFound`
  // while the name is really taken; ask whether it is a link separately.
  for (
    var ancestor = path.dirname(scriptsPath);
    path.isWithin(projectPath, ancestor);
    ancestor = path.dirname(ancestor)
  ) {
    final type = FileSystemEntity.typeSync(ancestor);
    if (type == FileSystemEntityType.notFound && FileSystemEntity.isLinkSync(ancestor)) {
      throw MerryError(type: ErrorCode.invalidScripts);
    }
    if (type != FileSystemEntityType.notFound && type != FileSystemEntityType.directory) {
      throw MerryError(type: ErrorCode.invalidScripts);
    }
  }

  // Every check above is lexical, and a write follows symlinks. Decide where
  // the write would actually land before doing it: resolving both sides makes
  // a link out of the project, a link onto the manifest, and a `scripts:
  // pubspec.yaml` that names it directly all the same rejection.
  final resolvedProject = await Directory(projectPath).resolveSymbolicLinks();
  final resolvedScripts = await _resolveWriteTarget(scriptsPath);
  final resolvedPubspec = await _resolveWriteTarget(pubspec.filePath);
  if (!path.isWithin(resolvedProject, resolvedScripts) || path.equals(resolvedScripts, resolvedPubspec)) {
    throw MerryError(type: ErrorCode.invalidScripts);
  }

  // Two names for one inode — a hard link — stay distinct however thoroughly
  // they are resolved, and writing through either truncates the manifest they
  // share. `dart:io` exposes no inode, so identity is inferred from content.
  if (existingType == FileSystemEntityType.file && await _hasManifestContent(scriptsPath, pubspec.filePath)) {
    throw MerryError(type: ErrorCode.invalidScripts);
  }

  final scriptsFile = File(scriptsPath);
  final linked = content.containsKey(scriptsKey);

  if (existingType == FileSystemEntityType.file && !_confirm('$target already exists. Replace its contents?')) {
    stdout.writeln('Nothing changed.');
    return 0;
  }

  await scriptsFile.parent.create(recursive: true);
  await scriptsFile.writeAsString(_buildTemplate(content, projectPath));

  if (!linked) await _linkPubspec(pubspec.filePath, target);

  // Load what was just written through the normal read path, so a broken
  // template or a botched pubspec edit fails here instead of on first use.
  await Pubspec(currentDirPath: projectPath).getScripts();

  stdout.writeln(
    linked ? 'Wrote $target.' : 'Wrote $target and linked it from $pubspecFileName.',
  );
  stdout.writeln('Run `merry ls` to see the available scripts.');
  return 0;
}