@graftland/cli 0.3.3 → 0.4.1
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 +3 -2
- package/README.zh-CN.md +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/Application.d.ts +27 -0
- package/dist/Application.d.ts.map +1 -0
- package/dist/Application.js +27 -0
- package/dist/Application.js.map +1 -0
- package/dist/Commands.d.ts +34 -0
- package/dist/Commands.d.ts.map +1 -0
- package/dist/Commands.js +31 -0
- package/dist/Commands.js.map +1 -0
- package/dist/Manage.d.ts +7 -10
- package/dist/Manage.d.ts.map +1 -1
- package/dist/Manage.js +61 -15
- package/dist/Manage.js.map +1 -1
- package/dist/Start.d.ts +2 -1
- package/dist/Start.d.ts.map +1 -1
- package/dist/Start.js +25 -9
- package/dist/Start.js.map +1 -1
- package/dist/bin.js +39 -16
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/internal/commandConfig.js +62 -0
- package/dist/internal/commandConfig.js.map +1 -0
- package/dist/internal/commandHost.js +80 -0
- package/dist/internal/commandHost.js.map +1 -0
- package/dist/internal/commandRegistry.js +59 -0
- package/dist/internal/commandRegistry.js.map +1 -0
- package/dist/internal/configSchema.d.ts +25 -4
- package/dist/internal/configSchema.d.ts.map +1 -1
- package/dist/internal/configSchema.js +6 -34
- package/dist/internal/configSchema.js.map +1 -1
- package/dist/internal/env.js.map +1 -1
- package/dist/internal/importer.js +18 -0
- package/dist/internal/importer.js.map +1 -0
- package/dist/internal/intercepts.js.map +1 -1
- package/dist/internal/protocolOutput.js +67 -0
- package/dist/internal/protocolOutput.js.map +1 -0
- package/dist/internal/watch.js.map +1 -1
- package/package.json +10 -7
- package/schema.json +2321 -0
- package/src/Application.ts +55 -0
- package/src/Commands.ts +47 -0
- package/src/Manage.ts +78 -19
- package/src/Start.ts +100 -28
- package/src/bin.ts +81 -34
- package/src/index.ts +3 -0
- package/src/internal/commandConfig.ts +91 -0
- package/src/internal/commandHost.ts +216 -0
- package/src/internal/commandRegistry.ts +100 -0
- package/src/internal/configSchema.ts +10 -70
- package/src/internal/env.ts +3 -2
- package/src/internal/importer.ts +28 -0
- package/src/internal/intercepts.ts +2 -1
- package/src/internal/protocolOutput.ts +101 -0
- package/src/internal/watch.ts +4 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Context,
|
|
3
|
+
Effect,
|
|
4
|
+
Exit,
|
|
5
|
+
Layer,
|
|
6
|
+
Option,
|
|
7
|
+
Schema,
|
|
8
|
+
Scope,
|
|
9
|
+
Stdio,
|
|
10
|
+
} from "effect"
|
|
11
|
+
import { Digest, SecureRandom, Signature } from "effect/crypto"
|
|
12
|
+
import { FetchHttpClient } from "effect/http"
|
|
13
|
+
import { NodeServices } from "@effect/platform-node"
|
|
14
|
+
|
|
15
|
+
import { ProtocolStdio } from "./internal/protocolOutput"
|
|
16
|
+
|
|
17
|
+
/** Set before argument parsing when stdout belongs to a byte protocol. */
|
|
18
|
+
export const ProtocolOutput = Context.Reference(
|
|
19
|
+
"@graftland/cli/ProtocolOutput",
|
|
20
|
+
{ defaultValue: () => false },
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
/** An application requested a failed process exit, preserving its original cause. */
|
|
24
|
+
export class ApplicationError extends Schema.TaggedError<
|
|
25
|
+
ApplicationError,
|
|
26
|
+
{ readonly brand: unique symbol }
|
|
27
|
+
>()("ApplicationError", {
|
|
28
|
+
cause: Schema.Cause(Schema.Unknown, Schema.Unknown),
|
|
29
|
+
}) {}
|
|
30
|
+
|
|
31
|
+
/** Applications can request process completion and report cleanup independently of loader diagnostics. */
|
|
32
|
+
export class Application extends Context.Service<
|
|
33
|
+
Application,
|
|
34
|
+
{
|
|
35
|
+
/** Exclusively reserves the process protocol channel until the caller releases its scope. */
|
|
36
|
+
readonly claimProtocol: Effect.Effect<void, ApplicationError, Scope.Scope>
|
|
37
|
+
readonly finish: (exit: Exit.Exit<void, unknown>) => Effect.Effect<void>
|
|
38
|
+
readonly onExit: (release: Effect.Effect<void>) => Effect.Effect<void>
|
|
39
|
+
}
|
|
40
|
+
>()("@graftland/cli/Application") {}
|
|
41
|
+
|
|
42
|
+
/** Node capabilities shared by application and command hosts. */
|
|
43
|
+
export const platform = Layer.mergeAll(
|
|
44
|
+
Layer.effect(
|
|
45
|
+
Stdio.Stdio,
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
const reserved = yield* ProtocolStdio
|
|
48
|
+
return Option.isSome(reserved) ? reserved.value : yield* Stdio.Stdio
|
|
49
|
+
}),
|
|
50
|
+
).pipe(Layer.provideMerge(NodeServices.layer)),
|
|
51
|
+
FetchHttpClient.layer,
|
|
52
|
+
Digest.layerWebCrypto,
|
|
53
|
+
SecureRandom.layerWebCrypto,
|
|
54
|
+
Signature.layerWebCrypto.pipe(Layer.provide(SecureRandom.layerWebCrypto)),
|
|
55
|
+
)
|
package/src/Commands.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Context, Effect, Schema, Scope } from "effect"
|
|
2
|
+
import type { CliError, Command } from "effect/cli"
|
|
3
|
+
|
|
4
|
+
/** Services available when a registered command runs. Provide other services on the command itself. */
|
|
5
|
+
export type Environment = Command.Environment | Scope.Scope
|
|
6
|
+
|
|
7
|
+
/** A command name, alias, or registration lifetime conflicts with the host. */
|
|
8
|
+
export class CommandRegistrationError extends Schema.TaggedError<
|
|
9
|
+
CommandRegistrationError,
|
|
10
|
+
{ readonly brand: unique symbol }
|
|
11
|
+
>()("CommandRegistrationError", {
|
|
12
|
+
command: Schema.String,
|
|
13
|
+
detail: Schema.String,
|
|
14
|
+
}) {
|
|
15
|
+
override get message() {
|
|
16
|
+
return `${this.command}: ${this.detail}`
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Register one native Effect CLI command tree during the plugin's activation. */
|
|
21
|
+
export class Commands extends Context.Service<
|
|
22
|
+
Commands,
|
|
23
|
+
{
|
|
24
|
+
readonly register: <
|
|
25
|
+
const Name extends string,
|
|
26
|
+
Input,
|
|
27
|
+
ContextInput,
|
|
28
|
+
E extends CliError.CliError,
|
|
29
|
+
R extends Environment,
|
|
30
|
+
>(
|
|
31
|
+
command: Command.Command<Name, Input, ContextInput, E, R>,
|
|
32
|
+
) => Effect.Effect<void, CommandRegistrationError, Scope.Scope>
|
|
33
|
+
}
|
|
34
|
+
>()("@graftland/cli/Commands") {}
|
|
35
|
+
|
|
36
|
+
/** A declared command plugin could not be decoded from its config file. */
|
|
37
|
+
export class CommandConfigError extends Schema.TaggedError<
|
|
38
|
+
CommandConfigError,
|
|
39
|
+
{ readonly brand: unique symbol }
|
|
40
|
+
>()("CommandConfigError", {
|
|
41
|
+
file: Schema.String,
|
|
42
|
+
cause: Schema.Unknown,
|
|
43
|
+
}) {
|
|
44
|
+
override get message() {
|
|
45
|
+
return `Cannot read command plugins from ${this.file}`
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/Manage.ts
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Changing a project's dependencies.
|
|
3
3
|
*/
|
|
4
|
-
import * as Create from "@graftland/create"
|
|
5
|
-
import * as PackageManager from "@graftland/package-manager"
|
|
6
4
|
import {
|
|
7
5
|
Array,
|
|
8
6
|
Config,
|
|
9
7
|
Effect,
|
|
8
|
+
FileSystem,
|
|
10
9
|
Option,
|
|
10
|
+
Path,
|
|
11
11
|
Predicate,
|
|
12
12
|
Record,
|
|
13
13
|
Schema,
|
|
14
14
|
} from "effect"
|
|
15
15
|
|
|
16
|
+
import * as Create from "@graftland/create"
|
|
17
|
+
import * as PackageManager from "@graftland/package-manager"
|
|
18
|
+
|
|
19
|
+
import { parseDocument, type Document } from "yaml"
|
|
20
|
+
|
|
21
|
+
import { CommandConfigError } from "./Commands"
|
|
22
|
+
import { home } from "./internal/commandConfig"
|
|
23
|
+
|
|
16
24
|
const report = (result: PackageManager.InstallResult) =>
|
|
17
25
|
Effect.logInfo(
|
|
18
26
|
`${result.agent}: ${result.added.length} added, ${result.updated.length} updated, ${result.removed.length} removed`,
|
|
@@ -58,22 +66,6 @@ const install = Effect.fn("Manage.install")(function* (
|
|
|
58
66
|
yield* report(result)
|
|
59
67
|
})
|
|
60
68
|
|
|
61
|
-
export const create = Effect.fn("Manage.create")(function* (options: {
|
|
62
|
-
readonly directory: string
|
|
63
|
-
readonly name: Option.Option<string>
|
|
64
|
-
readonly install: boolean
|
|
65
|
-
}) {
|
|
66
|
-
const project = yield* Create.create({
|
|
67
|
-
directory: options.directory,
|
|
68
|
-
...Option.match(options.name, {
|
|
69
|
-
onNone: () => ({}),
|
|
70
|
-
onSome: (name) => ({ name }),
|
|
71
|
-
}),
|
|
72
|
-
})
|
|
73
|
-
yield* Effect.logInfo(`Created ${project.name} in ${project.directory}`)
|
|
74
|
-
if (options.install) yield* install(project.directory, {})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
69
|
export const add = Effect.fn("Manage.add")(function* (
|
|
78
70
|
specs: ReadonlyArray<string>,
|
|
79
71
|
) {
|
|
@@ -95,3 +87,70 @@ export const remove = (names: ReadonlyArray<string>) =>
|
|
|
95
87
|
".",
|
|
96
88
|
Record.fromEntries(Array.map(names, (name) => [name, null] as const)),
|
|
97
89
|
)
|
|
90
|
+
|
|
91
|
+
const enabledConfig = Effect.fnUntraced(function* (
|
|
92
|
+
file: string,
|
|
93
|
+
module: string,
|
|
94
|
+
) {
|
|
95
|
+
const fs = yield* FileSystem.FileSystem
|
|
96
|
+
const text = (yield* fs.exists(file))
|
|
97
|
+
? yield* fs.readFileString(file)
|
|
98
|
+
: "[]\n"
|
|
99
|
+
const document = yield* Effect.try({
|
|
100
|
+
try: () => {
|
|
101
|
+
const document: Document = parseDocument(text)
|
|
102
|
+
const error = document.errors[0]
|
|
103
|
+
if (error !== undefined) throw error
|
|
104
|
+
if (document.toJS() === null) document.contents = document.createNode([])
|
|
105
|
+
return document
|
|
106
|
+
},
|
|
107
|
+
catch: (cause) => CommandConfigError.make({ file, cause }),
|
|
108
|
+
})
|
|
109
|
+
const entries = yield* Schema.decodeUnknownEffect(
|
|
110
|
+
Schema.Array(Schema.Unknown),
|
|
111
|
+
)(document.toJS())
|
|
112
|
+
const existing = Array.findFirstIndex(
|
|
113
|
+
entries,
|
|
114
|
+
(entry) =>
|
|
115
|
+
Predicate.hasProperty(entry, "name") &&
|
|
116
|
+
entry.name === module &&
|
|
117
|
+
Predicate.hasProperty(entry, "cli") &&
|
|
118
|
+
entry.cli === true,
|
|
119
|
+
)
|
|
120
|
+
return yield* Effect.try({
|
|
121
|
+
try: () => {
|
|
122
|
+
if (Option.isSome(existing))
|
|
123
|
+
document.setIn([existing.value, "disabled"], false)
|
|
124
|
+
else document.add({ name: module, cli: true })
|
|
125
|
+
return document.toString()
|
|
126
|
+
},
|
|
127
|
+
catch: (cause) => CommandConfigError.make({ file, cause }),
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
const prepareHome = Effect.fnUntraced(function* (directory: string) {
|
|
131
|
+
const fs = yield* FileSystem.FileSystem
|
|
132
|
+
const path = yield* Path.Path
|
|
133
|
+
yield* fs.makeDirectory(directory, { recursive: true })
|
|
134
|
+
const manifest = path.join(directory, "package.json")
|
|
135
|
+
if (!(yield* fs.exists(manifest)))
|
|
136
|
+
yield* fs.writeFileString(manifest, '{"private":true,"type":"module"}\n')
|
|
137
|
+
}, Effect.uninterruptible)
|
|
138
|
+
|
|
139
|
+
/** Install a command plugin and explicitly enable its /cli entry. */
|
|
140
|
+
export const addPlugin = Effect.fn("Manage.addPlugin")(function* (
|
|
141
|
+
name: string,
|
|
142
|
+
global: boolean = false,
|
|
143
|
+
) {
|
|
144
|
+
const path = yield* Path.Path
|
|
145
|
+
const fs = yield* FileSystem.FileSystem
|
|
146
|
+
const directory = global ? yield* home : path.resolve(".")
|
|
147
|
+
const packageName = Create.pluginPackage(name)
|
|
148
|
+
const module = `${packageName}/cli`
|
|
149
|
+
const file = path.join(directory, "graft.yml")
|
|
150
|
+
const text = yield* enabledConfig(file, module)
|
|
151
|
+
if (global) yield* prepareHome(directory)
|
|
152
|
+
yield* install(directory, Record.singleton(packageName, "latest"))
|
|
153
|
+
// Once accepted, the native write must finish before command cleanup can proceed.
|
|
154
|
+
yield* fs.writeFileString(file, text).pipe(Effect.uninterruptible)
|
|
155
|
+
yield* Effect.logInfo(`Enabled ${module} in ${file}`)
|
|
156
|
+
})
|
package/src/Start.ts
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runs a Graft application from its config file.
|
|
3
3
|
*/
|
|
4
|
+
import {
|
|
5
|
+
Array,
|
|
6
|
+
Cause,
|
|
7
|
+
Deferred,
|
|
8
|
+
Effect,
|
|
9
|
+
Exit,
|
|
10
|
+
FileSystem,
|
|
11
|
+
Layer,
|
|
12
|
+
Logger,
|
|
13
|
+
Option,
|
|
14
|
+
Path,
|
|
15
|
+
Scope,
|
|
16
|
+
Semaphore,
|
|
17
|
+
type PlatformError,
|
|
18
|
+
type Schema,
|
|
19
|
+
} from "effect"
|
|
4
20
|
import { NodeServices } from "@effect/platform-node"
|
|
5
|
-
|
|
21
|
+
|
|
6
22
|
import { Graft, Plugin } from "@graftland/graft"
|
|
7
23
|
import * as Hmr from "@graftland/hmr"
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
24
|
+
import {
|
|
25
|
+
Entry,
|
|
26
|
+
Include,
|
|
27
|
+
Loader,
|
|
28
|
+
ModuleResolver,
|
|
29
|
+
type Tree,
|
|
30
|
+
} from "@graftland/loader"
|
|
31
|
+
|
|
32
|
+
import { Application, ApplicationError, platform } from "./Application"
|
|
33
|
+
import { layer as env, EnvFile, watchEnvFile } from "./internal/env"
|
|
12
34
|
import { layer as intercepts } from "./internal/intercepts"
|
|
13
35
|
import { watchModules } from "./internal/watch"
|
|
14
36
|
|
|
@@ -29,18 +51,48 @@ export interface Options {
|
|
|
29
51
|
* modules are imported through the hot reloader, so bare specifiers resolve
|
|
30
52
|
* from the application directory whether or not `watch` is on.
|
|
31
53
|
*/
|
|
32
|
-
export const run
|
|
54
|
+
export const run = (
|
|
33
55
|
options: Options,
|
|
34
|
-
)
|
|
35
|
-
|
|
56
|
+
): Effect.Effect<
|
|
57
|
+
void,
|
|
58
|
+
| ApplicationError
|
|
36
59
|
| Graft.ServiceConflict
|
|
37
60
|
| PlatformError.BadArgument
|
|
38
61
|
| PlatformError.PlatformError
|
|
39
62
|
| Schema.SchemaError
|
|
40
63
|
| Loader.StoreError
|
|
41
64
|
| Tree.InvalidOperation
|
|
42
|
-
>
|
|
43
|
-
function* (
|
|
65
|
+
> =>
|
|
66
|
+
Effect.gen(function* () {
|
|
67
|
+
const scope = yield* Effect.scope
|
|
68
|
+
const done = yield* Deferred.make<void, ApplicationError>()
|
|
69
|
+
const protocol = yield* Semaphore.make(1)
|
|
70
|
+
yield* Graft.provide(Application, {
|
|
71
|
+
claimProtocol: Effect.acquireRelease(
|
|
72
|
+
protocol.takeIfAvailable(1).pipe(
|
|
73
|
+
Effect.flatMap((acquired) =>
|
|
74
|
+
acquired
|
|
75
|
+
? Effect.void
|
|
76
|
+
: Effect.fail(
|
|
77
|
+
ApplicationError.make({
|
|
78
|
+
cause: Cause.fail(
|
|
79
|
+
"Only one runner may own the process stdio channel",
|
|
80
|
+
),
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
() => protocol.release(1),
|
|
86
|
+
),
|
|
87
|
+
finish: (exit) =>
|
|
88
|
+
Deferred.done(
|
|
89
|
+
done,
|
|
90
|
+
Exit.isFailure(exit)
|
|
91
|
+
? Exit.fail(ApplicationError.make({ cause: exit.cause }))
|
|
92
|
+
: Exit.void,
|
|
93
|
+
).pipe(Effect.asVoid),
|
|
94
|
+
onExit: (release) => Scope.addFinalizer(scope, release),
|
|
95
|
+
})
|
|
44
96
|
const path = yield* Path.Path
|
|
45
97
|
const file = path.resolve(options.config)
|
|
46
98
|
const directory = path.dirname(file)
|
|
@@ -52,16 +104,37 @@ export const run: (
|
|
|
52
104
|
})
|
|
53
105
|
const modules = yield* ModuleResolver.make({
|
|
54
106
|
base: directory,
|
|
55
|
-
importer: hmr.importModule,
|
|
107
|
+
importer: options.watch ? hmr.importModuleScoped : hmr.importModule,
|
|
56
108
|
})
|
|
57
109
|
yield* Graft.provide(ModuleResolver.ModuleResolver, {
|
|
58
110
|
resolve: resolver.resolve,
|
|
59
111
|
importModule: modules.importModule,
|
|
60
112
|
})
|
|
113
|
+
const fs = yield* FileSystem.FileSystem
|
|
114
|
+
const observe = hmr.observation.pipe(
|
|
115
|
+
Effect.provideService(FileSystem.FileSystem, fs),
|
|
116
|
+
Effect.map((source) => ({
|
|
117
|
+
...source,
|
|
118
|
+
watch: (files: ReadonlyArray<string>, activation: Scope.Scope) =>
|
|
119
|
+
source.watch(files, activation).pipe(
|
|
120
|
+
Effect.mapError((cause) =>
|
|
121
|
+
ModuleResolver.ModuleInvalid.make({
|
|
122
|
+
specifier: Array.join(files, ", "),
|
|
123
|
+
cause,
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
})),
|
|
128
|
+
)
|
|
61
129
|
const included = yield* Graft.plugin(Include.Include, {
|
|
62
130
|
path: file,
|
|
63
131
|
watch: options.watch,
|
|
64
|
-
})
|
|
132
|
+
}).pipe(
|
|
133
|
+
Effect.provideService(
|
|
134
|
+
ModuleResolver.Watch,
|
|
135
|
+
options.watch ? Option.some(observe) : Option.none(),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
65
138
|
yield* included.await
|
|
66
139
|
if (options.watch) {
|
|
67
140
|
const { reload } = yield* EnvFile
|
|
@@ -69,16 +142,17 @@ export const run: (
|
|
|
69
142
|
const reloader = Plugin.make({
|
|
70
143
|
name: "reloader",
|
|
71
144
|
inject: [Loader.Loader, FileSystem.FileSystem, Path.Path],
|
|
72
|
-
apply:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
if (envFile !== undefined)
|
|
78
|
-
yield* watchEnvFile(envFile, reload, loader.retry).pipe(
|
|
145
|
+
apply: () =>
|
|
146
|
+
Effect.gen(function* () {
|
|
147
|
+
const loader = yield* Loader.Loader
|
|
148
|
+
yield* watchModules(hmr, directory, loader.retry).pipe(
|
|
79
149
|
Effect.forkScoped,
|
|
80
150
|
)
|
|
81
|
-
|
|
151
|
+
if (envFile !== undefined)
|
|
152
|
+
yield* watchEnvFile(envFile, reload, loader.retry).pipe(
|
|
153
|
+
Effect.forkScoped,
|
|
154
|
+
)
|
|
155
|
+
}),
|
|
82
156
|
})
|
|
83
157
|
yield* Graft.plugin(reloader)
|
|
84
158
|
}
|
|
@@ -86,22 +160,20 @@ export const run: (
|
|
|
86
160
|
yield* Effect.logInfo(
|
|
87
161
|
`Running ${path.relative(process.cwd(), file) || file}`,
|
|
88
162
|
)
|
|
89
|
-
return yield*
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
163
|
+
return yield* Deferred.await(done)
|
|
164
|
+
}).pipe(
|
|
165
|
+
Effect.scoped,
|
|
166
|
+
Effect.provideService(Entry.Select, (entry) => entry["cli"] !== true),
|
|
93
167
|
// @effect-diagnostics-next-line strictEffectProvide:off -- this runs the whole application, so it is an entry point
|
|
94
168
|
Effect.provide(
|
|
95
|
-
effect,
|
|
96
169
|
Layer.provideMerge(
|
|
97
170
|
Graft.layer,
|
|
98
171
|
Layer.mergeAll(
|
|
99
|
-
|
|
100
|
-
NodeServices.layer,
|
|
172
|
+
platform,
|
|
101
173
|
intercepts,
|
|
102
174
|
env(options.envFile).pipe(Layer.provide(NodeServices.layer)),
|
|
103
175
|
Logger.layer([Logger.consolePretty()]),
|
|
104
176
|
),
|
|
105
177
|
),
|
|
106
178
|
),
|
|
107
|
-
)
|
|
179
|
+
)
|
package/src/bin.ts
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The `graft` executable. Running this module starts the program.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { Array, Effect, Layer, Logger, Option, Stdio } from "effect"
|
|
6
|
+
import { Argument, CliError, Command, Flag } from "effect/cli"
|
|
7
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
8
|
+
|
|
9
|
+
import { command as create } from "@graftland/create/Command"
|
|
10
|
+
import { Graft } from "@graftland/graft"
|
|
11
|
+
|
|
8
12
|
import { version } from "../package.json" with { type: "json" }
|
|
13
|
+
import { platform } from "./Application"
|
|
14
|
+
import * as Host from "./internal/commandHost"
|
|
9
15
|
|
|
10
16
|
const start = Command.make(
|
|
11
17
|
"start",
|
|
@@ -40,36 +46,30 @@ const start = Command.make(
|
|
|
40
46
|
Command.withDescription("Run the application described by a config file"),
|
|
41
47
|
)
|
|
42
48
|
|
|
43
|
-
const create = Command.make(
|
|
44
|
-
"create",
|
|
45
|
-
{
|
|
46
|
-
directory: Argument.String("directory").pipe(
|
|
47
|
-
Argument.withDescription("Where to create the project"),
|
|
48
|
-
),
|
|
49
|
-
name: Flag.String("name").pipe(
|
|
50
|
-
Flag.withDescription("Package name; defaults to the directory name"),
|
|
51
|
-
Flag.optional,
|
|
52
|
-
),
|
|
53
|
-
install: Flag.Boolean("install").pipe(
|
|
54
|
-
Flag.withDescription("Install dependencies afterwards"),
|
|
55
|
-
Flag.withDefault(false),
|
|
56
|
-
),
|
|
57
|
-
},
|
|
58
|
-
(options) =>
|
|
59
|
-
Effect.promise(() => import("./Manage")).pipe(
|
|
60
|
-
Effect.uninterruptible,
|
|
61
|
-
Effect.flatMap((Manage) => Manage.create(options)),
|
|
62
|
-
),
|
|
63
|
-
).pipe(Command.withDescription("Create a new Graft project"))
|
|
64
|
-
|
|
65
49
|
const add = Command.make(
|
|
66
50
|
"add",
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
51
|
+
{
|
|
52
|
+
packages: Argument.variadic(Argument.String("package")),
|
|
53
|
+
plugin: Flag.String("plugin").pipe(Flag.optional),
|
|
54
|
+
global: Flag.Boolean("global").pipe(Flag.withDefault(false)),
|
|
55
|
+
},
|
|
56
|
+
Effect.fnUntraced(function* ({ packages, plugin, global }) {
|
|
57
|
+
if (
|
|
58
|
+
(global && Option.isNone(plugin)) ||
|
|
59
|
+
(Option.isSome(plugin) && packages.length > 0)
|
|
60
|
+
)
|
|
61
|
+
return yield* CliError.UserError.make({
|
|
62
|
+
cause: "Conflicting install options",
|
|
63
|
+
userMessage:
|
|
64
|
+
"Use add --plugin <name> [--global] without package arguments",
|
|
65
|
+
})
|
|
66
|
+
const Manage = yield* Effect.promise(() => import("./Manage")).pipe(
|
|
70
67
|
Effect.uninterruptible,
|
|
71
|
-
|
|
72
|
-
)
|
|
68
|
+
)
|
|
69
|
+
return yield* Option.isSome(plugin)
|
|
70
|
+
? Manage.addPlugin(plugin.value, global)
|
|
71
|
+
: Manage.add(packages)
|
|
72
|
+
}),
|
|
73
73
|
).pipe(
|
|
74
74
|
Command.withDescription(
|
|
75
75
|
"Add dependencies, restoring the project if installing fails",
|
|
@@ -92,10 +92,57 @@ const remove = Command.make(
|
|
|
92
92
|
|
|
93
93
|
const graft = Command.make("graft").pipe(
|
|
94
94
|
Command.withDescription("Run and manage Graft applications"),
|
|
95
|
-
Command.
|
|
95
|
+
Command.withSharedFlags({
|
|
96
|
+
stdio: Flag.Boolean("stdio").pipe(Flag.withDefault(false)),
|
|
97
|
+
noPlugins: Flag.Boolean("no-plugins").pipe(Flag.withDefault(false)),
|
|
98
|
+
}),
|
|
96
99
|
)
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
const program = Effect.gen(function* () {
|
|
102
|
+
const stdio = yield* Stdio.Stdio
|
|
103
|
+
const args = yield* stdio.args
|
|
104
|
+
return yield* Host.output(
|
|
105
|
+
Effect.scoped(
|
|
106
|
+
Effect.gen(function* () {
|
|
107
|
+
const context = yield* Layer.build(Graft.layer)
|
|
108
|
+
return yield* Host.run(
|
|
109
|
+
[start, create, add, remove],
|
|
110
|
+
graft,
|
|
111
|
+
version,
|
|
112
|
+
args,
|
|
113
|
+
).pipe(Effect.provideContext(context))
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
116
|
+
args,
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
NodeRuntime.runMain(
|
|
120
|
+
Effect.scoped(
|
|
121
|
+
Effect.gen(function* () {
|
|
122
|
+
const context = yield* Layer.build(platform)
|
|
123
|
+
return yield* program.pipe(Effect.provideContext(context))
|
|
124
|
+
}),
|
|
125
|
+
).pipe(
|
|
126
|
+
(effect) =>
|
|
127
|
+
Effect.withFiber((main) =>
|
|
128
|
+
effect.pipe(
|
|
129
|
+
Effect.tapCause((cause) =>
|
|
130
|
+
Array.every(
|
|
131
|
+
cause.reasons,
|
|
132
|
+
(reason) =>
|
|
133
|
+
reason._tag === "Interrupt" && reason.fiberId === main.id,
|
|
134
|
+
) ||
|
|
135
|
+
Array.every(
|
|
136
|
+
cause.reasons,
|
|
137
|
+
(reason) =>
|
|
138
|
+
reason._tag === "Fail" && CliError.isCliError(reason.error),
|
|
139
|
+
)
|
|
140
|
+
? Effect.void
|
|
141
|
+
: Effect.logError(cause),
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
Effect.provideService(Logger.LogToStderr, true),
|
|
146
|
+
),
|
|
147
|
+
{ disableErrorReporting: true },
|
|
101
148
|
)
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Array,
|
|
3
|
+
Config,
|
|
4
|
+
Effect,
|
|
5
|
+
FileSystem,
|
|
6
|
+
HashMap,
|
|
7
|
+
Option,
|
|
8
|
+
Path,
|
|
9
|
+
Predicate,
|
|
10
|
+
Schema,
|
|
11
|
+
} from "effect"
|
|
12
|
+
|
|
13
|
+
import { parseDocument } from "yaml"
|
|
14
|
+
|
|
15
|
+
import { CommandConfigError } from "../Commands"
|
|
16
|
+
|
|
17
|
+
const Description = Schema.Struct({
|
|
18
|
+
name: Schema.String,
|
|
19
|
+
config: Schema.optionalKey(Schema.Unknown),
|
|
20
|
+
disabled: Schema.optionalKey(Schema.Boolean),
|
|
21
|
+
})
|
|
22
|
+
export const home = Effect.gen(function* () {
|
|
23
|
+
const path = yield* Path.Path
|
|
24
|
+
const override = yield* Config.String("GRAFT_HOME").pipe(Config.option)
|
|
25
|
+
if (Option.isSome(override)) return path.resolve(override.value)
|
|
26
|
+
const directory = yield* Config.String("HOME")
|
|
27
|
+
return path.join(directory, ".graft")
|
|
28
|
+
})
|
|
29
|
+
export const read = Effect.fnUntraced(function* (file: string) {
|
|
30
|
+
const fs = yield* FileSystem.FileSystem
|
|
31
|
+
if (!(yield* fs.exists(file))) return []
|
|
32
|
+
const text = yield* fs.readFileString(file)
|
|
33
|
+
const { Entry } = yield* Effect.promise(
|
|
34
|
+
() => import("@graftland/loader"),
|
|
35
|
+
).pipe(Effect.uninterruptible)
|
|
36
|
+
const value = yield* Effect.try({
|
|
37
|
+
try: () => {
|
|
38
|
+
const document = parseDocument(text, {
|
|
39
|
+
customTags: [
|
|
40
|
+
{ tag: "!env", resolve: (name: string) => new Entry.Env({ name }) },
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
const error = document.errors[0]
|
|
44
|
+
if (error !== undefined) throw error
|
|
45
|
+
return document.toJS()
|
|
46
|
+
},
|
|
47
|
+
catch: (cause) => CommandConfigError.make({ file, cause }),
|
|
48
|
+
})
|
|
49
|
+
const entries = yield* Schema.decodeUnknownEffect(
|
|
50
|
+
Schema.NullOr(Schema.Array(Schema.Unknown)),
|
|
51
|
+
)(value).pipe(
|
|
52
|
+
Effect.mapError((cause) => CommandConfigError.make({ file, cause })),
|
|
53
|
+
)
|
|
54
|
+
return yield* Effect.forEach(
|
|
55
|
+
Array.filter(
|
|
56
|
+
entries ?? [],
|
|
57
|
+
(entry) => Predicate.hasProperty(entry, "cli") && entry.cli === true,
|
|
58
|
+
),
|
|
59
|
+
(entry) =>
|
|
60
|
+
Schema.decodeUnknownEffect(Description)(entry).pipe(
|
|
61
|
+
Effect.mapBoth({
|
|
62
|
+
onFailure: (cause) => CommandConfigError.make({ file, cause }),
|
|
63
|
+
onSuccess: (entry) => ({ ...entry, file }),
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
export const discover = Effect.fnUntraced(function* (watch: boolean) {
|
|
69
|
+
const path = yield* Path.Path
|
|
70
|
+
const directory = yield* home
|
|
71
|
+
const global = yield* read(path.join(directory, "graft.yml"))
|
|
72
|
+
const project = yield* read(path.resolve("graft.yml")).pipe(
|
|
73
|
+
Effect.catchTag("CommandConfigError", (error) =>
|
|
74
|
+
watch
|
|
75
|
+
? Effect.logWarning(
|
|
76
|
+
"Command discovery skipped invalid project configuration; restart to load command plugins",
|
|
77
|
+
error,
|
|
78
|
+
).pipe(Effect.as([]))
|
|
79
|
+
: Effect.fail(error),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
const merged = Array.reduce(
|
|
83
|
+
[...global, ...project],
|
|
84
|
+
HashMap.empty<string, (typeof global)[number]>(),
|
|
85
|
+
(map, entry) => HashMap.set(map, entry.name, entry),
|
|
86
|
+
)
|
|
87
|
+
return Array.filter(
|
|
88
|
+
Array.fromIterable(HashMap.values(merged)),
|
|
89
|
+
(entry) => entry.disabled !== true,
|
|
90
|
+
)
|
|
91
|
+
})
|