run method
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;
}