Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions GraphcodeKit/Sources/CLI/GraphcodeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import MailroomKit
/// it for that.
public enum GraphcodeCommand: Equatable, Sendable {
case help
case listProjects
/// `json` prints a JSON array for tools that enumerate projects and workspaces.
case listProjects(json: Bool)
/// The support directories `GRAPHCODE_SUPPORT_DIR` can point every other verb at, read
/// from disk by `Workspace.all()` — no daemon, since each workspace has its own.
case listWorkspaces
case listWorkspaces(json: Bool)
case status(projectPath: String)
/// `into` is the composite whose sub-graph the loop belongs in, when one was named —
/// the command is the same, addressed at a nested graph instead of this one.
Expand Down Expand Up @@ -90,9 +91,10 @@ public enum GraphcodeCommand: Equatable, Sendable {
graphcode — drive a graph of loops from the shell.

USAGE
graphcode projects
graphcode workspaces every workspace on this machine, * marking the current
one; point GRAPHCODE_SUPPORT_DIR at a path to use it
graphcode projects [--json]
graphcode workspaces [--json] every workspace on this machine, * marking the
current one; point GRAPHCODE_SUPPORT_DIR at a path to use it.
--json prints either list as a JSON array
graphcode status <project-path>
graphcode node create <project-path> --title <t> --type <main|turn|goal|time|composite> [options]
graphcode node stop <project-path> <node-id>
Expand Down Expand Up @@ -312,13 +314,14 @@ public enum GraphcodeCommand: Equatable, Sendable {
return .help

case "projects":
try validateFlags(arguments, allowed: [])
return .listProjects
if arguments.contains(where: isHelpFlag) { throw HelpRequested() }
try validateFlags(arguments, allowed: ["json"])
return .listProjects(json: arguments.contains("--json"))

case "workspaces":
if arguments.contains(where: isHelpFlag) { throw HelpRequested() }
try validateFlags(arguments, allowed: [])
return .listWorkspaces
try validateFlags(arguments, allowed: ["json"])
return .listWorkspaces(json: arguments.contains("--json"))

case "status":
let path = try take(&arguments, name: "project-path")
Expand Down Expand Up @@ -883,6 +886,28 @@ extension GraphcodeCommand {
.joined(separator: "\n")
}

public static func renderJSON(_ projects: [ProjectRef]) -> String {
encodeListJSON(projects)
}

public static func renderJSON(_ workspaces: [Workspace], current: Workspace) -> String {
struct Row: Encodable {
var name: String
var path: String
var current: Bool
}
return encodeListJSON(
workspaces.map { Row(name: $0.name, path: $0.url.path, current: $0.id == current.id) })
}

private static func encodeListJSON(_ rows: some Encodable) -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
encoder.dateEncodingStrategy = .iso8601
guard let data = try? encoder.encode(rows) else { return "[]" }
return String(decoding: data, as: UTF8.self)
}

/// The room for a terminal, from the mailbox the daemon answered
/// (`DaemonCommand.mailbox`). `mail list` asks for the whole room (`unread` false);
/// `mail inbox` asks for what the reading loop's cursor has not covered. The
Expand Down
11 changes: 7 additions & 4 deletions graphcode-cli/Sources/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ if case .help = command {

// Before the daemon too: the list is read from disk, and the daemon this would dial
// belongs to just one of the workspaces it lists.
if case .listWorkspaces = command {
print(GraphcodeCommand.render(Workspace.all(), current: .current))
if case .listWorkspaces(let json) = command {
print(
json
? GraphcodeCommand.renderJSON(Workspace.all(), current: .current)
: GraphcodeCommand.render(Workspace.all(), current: .current))
exit(0)
}

Expand Down Expand Up @@ -218,13 +221,13 @@ do {
case .help:
break

case .listProjects:
case .listProjects(let json):
try sendCommand(.listRecentProjects)
let event = try client.waitForEvent {
if case .recentProjectsListed = $0 { return true } else { return false }
}
if case .recentProjectsListed(let projects) = event {
print(GraphcodeCommand.render(projects))
print(json ? GraphcodeCommand.renderJSON(projects) : GraphcodeCommand.render(projects))
}

case .status(let projectPath):
Expand Down
93 changes: 93 additions & 0 deletions graphcode/Tests/GraphcodeListingCommandTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Foundation
import GraphcodeKit
import Testing

/// `graphcode projects` and `graphcode workspaces`: the CLI's ways to find what every
/// other verb addresses — project paths, and the support directories
/// `GRAPHCODE_SUPPORT_DIR` can point those verbs at. `--json` is for tools that
/// enumerate both.
@Suite
struct GraphcodeListingCommandTests {
@Test
func workspacesTakesOnlyJSON() throws {
#expect(try GraphcodeCommand.parse(["workspaces"]) == .listWorkspaces(json: false))
#expect(try GraphcodeCommand.parse(["workspaces", "--json"]) == .listWorkspaces(json: true))
#expect(throws: GraphcodeCommand.ParseError.unknownOption("--yaml")) {
try GraphcodeCommand.parse(["workspaces", "--yaml"])
}
}

@Test
func projectsTakesOnlyJSON() throws {
#expect(try GraphcodeCommand.parse(["projects"]) == .listProjects(json: false))
#expect(try GraphcodeCommand.parse(["projects", "--json"]) == .listProjects(json: true))
#expect(throws: GraphcodeCommand.ParseError.unknownOption("--jsn")) {
try GraphcodeCommand.parse(["projects", "--jsn"])
}
}

@Test
func askingEitherListForHelpShowsHelp() throws {
#expect(try GraphcodeCommand.parse(["workspaces", "--help"]) == .help)
#expect(try GraphcodeCommand.parse(["workspaces", "-h"]) == .help)
#expect(try GraphcodeCommand.parse(["projects", "--help"]) == .help)
#expect(try GraphcodeCommand.parse(["projects", "-h"]) == .help)
}

@Test
func eachWorkspacePrintsItsNameAndPathWithTheCurrentOneMarked() {
let (main, work) = Self.workspaces
#expect(
GraphcodeCommand.render([main, work], current: work)
== " Default /Users/me/.graphcode\n* work /Users/me/.graphcode-work")
}

@Test
func workspacesAsJSONCarryNamePathAndWhichIsCurrent() throws {
struct Row: Decodable, Equatable {
var name: String
var path: String
var current: Bool
}
let (main, work) = Self.workspaces
let json = GraphcodeCommand.renderJSON([main, work], current: work)
#expect(
try JSONDecoder().decode([Row].self, from: Data(json.utf8)) == [
Row(name: "Default", path: "/Users/me/.graphcode", current: false),
Row(name: "work", path: "/Users/me/.graphcode-work", current: true),
])
}

@Test
func projectsAsJSONCarryNamePathAndLastOpened() throws {
let project = ProjectRef(
path: "/Users/me/src/app", name: "app", lastOpenedAt: Date(timeIntervalSince1970: 0))
let json = GraphcodeCommand.renderJSON([project])
let rows = try #require(
try JSONSerialization.jsonObject(with: Data(json.utf8)) as? [[String: String]])
#expect(
rows == [
["name": "app", "path": "/Users/me/src/app", "lastOpenedAt": "1970-01-01T00:00:00Z"]
])
}

/// "no projects yet" is for a person; a tool needs an array it can iterate.
@Test
func noProjectsAsJSONIsAnEmptyArray() {
#expect(GraphcodeCommand.renderJSON([ProjectRef]()) == "[]")
}

@Test
func theHelpNamesBothListsAndTheirJSON() {
#expect(GraphcodeCommand.helpText.contains("graphcode projects [--json]"))
#expect(GraphcodeCommand.helpText.contains("graphcode workspaces [--json]"))
}

private static var workspaces: (Workspace, Workspace) {
let home = URL(fileURLWithPath: "/Users/me", isDirectory: true)
return (
Workspace(slug: "", url: home.appendingPathComponent(".graphcode")),
Workspace(slug: "work", url: home.appendingPathComponent(".graphcode-work"))
)
}
}
38 changes: 0 additions & 38 deletions graphcode/Tests/GraphcodeWorkspacesCommandTests.swift

This file was deleted.

Loading