@projitect/cli-internals 0.0.0-stage → 1.0.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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/dist/applier.d.ts +12 -0
  3. package/dist/applier.d.ts.map +1 -0
  4. package/dist/applier.js +69 -0
  5. package/dist/applier.js.map +1 -0
  6. package/dist/commands/add.d.ts +34 -0
  7. package/dist/commands/add.d.ts.map +1 -0
  8. package/dist/commands/add.js +64 -0
  9. package/dist/commands/add.js.map +1 -0
  10. package/dist/commands/build.d.ts +39 -0
  11. package/dist/commands/build.d.ts.map +1 -0
  12. package/dist/commands/build.js +108 -0
  13. package/dist/commands/build.js.map +1 -0
  14. package/dist/commands/dispatch.d.ts +5 -0
  15. package/dist/commands/dispatch.d.ts.map +1 -0
  16. package/dist/commands/dispatch.js +211 -0
  17. package/dist/commands/dispatch.js.map +1 -0
  18. package/dist/commands/explain.d.ts +10 -0
  19. package/dist/commands/explain.d.ts.map +1 -0
  20. package/dist/commands/explain.js +46 -0
  21. package/dist/commands/explain.js.map +1 -0
  22. package/dist/commands/init.d.ts +34 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +153 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/inspect-json.d.ts +51 -0
  27. package/dist/commands/inspect-json.d.ts.map +1 -0
  28. package/dist/commands/inspect-json.js +58 -0
  29. package/dist/commands/inspect-json.js.map +1 -0
  30. package/dist/commands/inspect.d.ts +41 -0
  31. package/dist/commands/inspect.d.ts.map +1 -0
  32. package/dist/commands/inspect.js +74 -0
  33. package/dist/commands/inspect.js.map +1 -0
  34. package/dist/commands/remodel.d.ts +22 -0
  35. package/dist/commands/remodel.d.ts.map +1 -0
  36. package/dist/commands/remodel.js +49 -0
  37. package/dist/commands/remodel.js.map +1 -0
  38. package/dist/config-cascade.d.ts +25 -0
  39. package/dist/config-cascade.d.ts.map +1 -0
  40. package/dist/config-cascade.js +45 -0
  41. package/dist/config-cascade.js.map +1 -0
  42. package/dist/differ.d.ts +31 -0
  43. package/dist/differ.d.ts.map +1 -0
  44. package/dist/differ.js +134 -0
  45. package/dist/differ.js.map +1 -0
  46. package/dist/edit-pjt.d.ts +19 -0
  47. package/dist/edit-pjt.d.ts.map +1 -0
  48. package/dist/edit-pjt.js +40 -0
  49. package/dist/edit-pjt.js.map +1 -0
  50. package/dist/filesystem-impl.d.ts +25 -0
  51. package/dist/filesystem-impl.d.ts.map +1 -0
  52. package/dist/filesystem-impl.js +153 -0
  53. package/dist/filesystem-impl.js.map +1 -0
  54. package/dist/git.d.ts +29 -0
  55. package/dist/git.d.ts.map +1 -0
  56. package/dist/git.js +63 -0
  57. package/dist/git.js.map +1 -0
  58. package/dist/index.d.ts +22 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +21 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/ipc/client.d.ts +11 -0
  63. package/dist/ipc/client.d.ts.map +1 -0
  64. package/dist/ipc/client.js +45 -0
  65. package/dist/ipc/client.js.map +1 -0
  66. package/dist/ipc/index.d.ts +5 -0
  67. package/dist/ipc/index.d.ts.map +1 -0
  68. package/dist/ipc/index.js +5 -0
  69. package/dist/ipc/index.js.map +1 -0
  70. package/dist/ipc/protocol.d.ts +251 -0
  71. package/dist/ipc/protocol.d.ts.map +1 -0
  72. package/dist/ipc/protocol.js +41 -0
  73. package/dist/ipc/protocol.js.map +1 -0
  74. package/dist/ipc/server.d.ts +9 -0
  75. package/dist/ipc/server.d.ts.map +1 -0
  76. package/dist/ipc/server.js +23 -0
  77. package/dist/ipc/server.js.map +1 -0
  78. package/dist/ipc/transport.d.ts +12 -0
  79. package/dist/ipc/transport.d.ts.map +1 -0
  80. package/dist/ipc/transport.js +19 -0
  81. package/dist/ipc/transport.js.map +1 -0
  82. package/dist/loader.d.ts +45 -0
  83. package/dist/loader.d.ts.map +1 -0
  84. package/dist/loader.js +65 -0
  85. package/dist/loader.js.map +1 -0
  86. package/dist/lockfile.d.ts +22 -0
  87. package/dist/lockfile.d.ts.map +1 -0
  88. package/dist/lockfile.js +59 -0
  89. package/dist/lockfile.js.map +1 -0
  90. package/dist/permissions.d.ts +20 -0
  91. package/dist/permissions.d.ts.map +1 -0
  92. package/dist/permissions.js +86 -0
  93. package/dist/permissions.js.map +1 -0
  94. package/dist/plan.d.ts +278 -0
  95. package/dist/plan.d.ts.map +1 -0
  96. package/dist/plan.js +212 -0
  97. package/dist/plan.js.map +1 -0
  98. package/dist/platform/file-system.d.ts +3 -0
  99. package/dist/platform/file-system.d.ts.map +1 -0
  100. package/dist/platform/file-system.js +109 -0
  101. package/dist/platform/file-system.js.map +1 -0
  102. package/dist/platform/index.d.ts +17 -0
  103. package/dist/platform/index.d.ts.map +1 -0
  104. package/dist/platform/index.js +20 -0
  105. package/dist/platform/index.js.map +1 -0
  106. package/dist/platform/process-runner.d.ts +41 -0
  107. package/dist/platform/process-runner.d.ts.map +1 -0
  108. package/dist/platform/process-runner.js +120 -0
  109. package/dist/platform/process-runner.js.map +1 -0
  110. package/dist/platform/stdio.d.ts +9 -0
  111. package/dist/platform/stdio.d.ts.map +1 -0
  112. package/dist/platform/stdio.js +11 -0
  113. package/dist/platform/stdio.js.map +1 -0
  114. package/dist/platform/terminal.d.ts +3 -0
  115. package/dist/platform/terminal.d.ts.map +1 -0
  116. package/dist/platform/terminal.js +81 -0
  117. package/dist/platform/terminal.js.map +1 -0
  118. package/dist/pm.d.ts +36 -0
  119. package/dist/pm.d.ts.map +1 -0
  120. package/dist/pm.js +95 -0
  121. package/dist/pm.js.map +1 -0
  122. package/dist/post-apply.d.ts +20 -0
  123. package/dist/post-apply.d.ts.map +1 -0
  124. package/dist/post-apply.js +29 -0
  125. package/dist/post-apply.js.map +1 -0
  126. package/dist/project-file.d.ts +8 -0
  127. package/dist/project-file.d.ts.map +1 -0
  128. package/dist/project-file.js +12 -0
  129. package/dist/project-file.js.map +1 -0
  130. package/dist/region.d.ts +105 -0
  131. package/dist/region.d.ts.map +1 -0
  132. package/dist/region.js +94 -0
  133. package/dist/region.js.map +1 -0
  134. package/dist/remover.d.ts +16 -0
  135. package/dist/remover.d.ts.map +1 -0
  136. package/dist/remover.js +88 -0
  137. package/dist/remover.js.map +1 -0
  138. package/package.json +52 -3
  139. package/src/applier.ts +91 -0
  140. package/src/commands/add.ts +88 -0
  141. package/src/commands/build.ts +156 -0
  142. package/src/commands/dispatch.ts +345 -0
  143. package/src/commands/explain.ts +81 -0
  144. package/src/commands/init.ts +233 -0
  145. package/src/commands/inspect-json.ts +68 -0
  146. package/src/commands/inspect.ts +106 -0
  147. package/src/commands/remodel.ts +72 -0
  148. package/src/config-cascade.ts +60 -0
  149. package/src/differ.ts +188 -0
  150. package/src/edit-pjt.ts +56 -0
  151. package/src/filesystem-impl.ts +230 -0
  152. package/src/git.ts +87 -0
  153. package/src/index.ts +54 -0
  154. package/src/ipc/client.ts +108 -0
  155. package/src/ipc/index.ts +12 -0
  156. package/src/ipc/protocol.ts +48 -0
  157. package/src/ipc/server.ts +62 -0
  158. package/src/ipc/transport.ts +28 -0
  159. package/src/loader.ts +115 -0
  160. package/src/lockfile.ts +81 -0
  161. package/src/permissions.ts +106 -0
  162. package/src/plan.ts +459 -0
  163. package/src/platform/file-system.ts +155 -0
  164. package/src/platform/index.ts +26 -0
  165. package/src/platform/process-runner.ts +150 -0
  166. package/src/platform/stdio.ts +11 -0
  167. package/src/platform/terminal.ts +89 -0
  168. package/src/pm.ts +163 -0
  169. package/src/post-apply.ts +45 -0
  170. package/src/project-file.ts +15 -0
  171. package/src/region.ts +139 -0
  172. package/src/remover.ts +127 -0
  173. package/README.md +0 -3
@@ -0,0 +1,345 @@
1
+ import { Effect, Match, Option, Terminal } from "effect"
2
+ import type { Prompt } from "effect/unstable/cli"
3
+ import { Argument, Command, Flag } from "effect/unstable/cli"
4
+ import type { Errors, ProjitectConfig } from "@projitect/core"
5
+ import { chooseSectionsInteractive } from "@projitect/blueprint"
6
+ import type { SectionStrategy } from "@projitect/blueprint"
7
+ import { parseEnv, resolveConfig } from "../config-cascade.js"
8
+ import { inspect, renderInspectJson } from "./inspect.js"
9
+ import { InspectJsonSchemaUrl } from "./inspect-json.js"
10
+ import { remodel } from "./remodel.js"
11
+ import { build, renderBuildResult } from "./build.js"
12
+ import { init } from "./init.js"
13
+ import { explain } from "./explain.js"
14
+ import { add } from "./add.js"
15
+
16
+ /**
17
+ * Per-handler error renderer. Catches our typed `ProjitectError` union and writes a
18
+ * consistent `Error [id]: message` line plus the docs URL / explain hint — or, with `json`, an
19
+ * `{ "error": { ... } }` object carrying the error's id and fields. Sets `process.exitCode = 1`
20
+ * so the CLI exits non-zero. Returns void so the handler chain stays happy.
21
+ */
22
+ const reportError = (
23
+ error: Errors.ProjitectError,
24
+ json = false,
25
+ ): Effect.Effect<void, never, Terminal.Terminal> =>
26
+ Effect.gen(function* () {
27
+ if (json) {
28
+ yield* display(`${JSON.stringify({ error }, null, 2)}\n`)
29
+ } else {
30
+ yield* display(`Error [${error.id}]: ${error.message}\n`)
31
+ yield* display(
32
+ ` See https://projitect.dev/errors/${error.id} or run \`pjt explain ${error.id}\`\n`,
33
+ )
34
+ }
35
+ process.exitCode = 1
36
+ })
37
+
38
+ const jsonFlag = (description: string) =>
39
+ Flag.boolean("json").pipe(Flag.withDefault(false), Flag.withDescription(description))
40
+
41
+ const display = (text: string): Effect.Effect<void, never, Terminal.Terminal> =>
42
+ Effect.gen(function* () {
43
+ const terminal = yield* Terminal.Terminal
44
+ // Best-effort write; PlatformError on display is non-actionable from a handler
45
+ yield* Effect.ignore(terminal.display(text))
46
+ })
47
+
48
+ const configFromEnv = (): ProjitectConfig.ProjitectConfig =>
49
+ resolveConfig({
50
+ env: parseEnv(process.env),
51
+ cliArgs: { projectRoot: process.cwd() },
52
+ })
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // pjt init
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const initCmd = Command.make(
59
+ "init",
60
+ {
61
+ yes: Flag.boolean("yes").pipe(
62
+ Flag.withDefault(false),
63
+ Flag.withDescription(
64
+ "Auto-bootstrap missing `.git/` (via `git init`) and `package.json` (minimal stub).",
65
+ ),
66
+ ),
67
+ },
68
+ (input) =>
69
+ Effect.gen(function* () {
70
+ const config = configFromEnv()
71
+ const result = yield* init({ config, yes: input.yes }).pipe(
72
+ Effect.matchEffect({
73
+ onSuccess: (r) => Effect.succeed(r),
74
+ onFailure: (error: Errors.ProjitectError) => reportError(error).pipe(Effect.as(null)),
75
+ }),
76
+ )
77
+ if (result === null) {
78
+ return
79
+ }
80
+ const lines: string[] = []
81
+ if (result.bootstrappedGit) {
82
+ lines.push("Initialized git repo (.git/)")
83
+ }
84
+ if (result.bootstrappedPackageJson) {
85
+ lines.push("Created package.json")
86
+ }
87
+ if (result.seededBlueprintFile) {
88
+ lines.push(`Created ${config.blueprintFile}`)
89
+ }
90
+ if (result.remodel.written.length > 0) {
91
+ lines.push(
92
+ `Wrote ${result.remodel.written.length} file${result.remodel.written.length === 1 ? "" : "s"}:`,
93
+ ...result.remodel.written.map((p) => ` ${p}`),
94
+ )
95
+ }
96
+ lines.push(
97
+ "",
98
+ "projitect initialized. Edit `.pjt.ts` to add blueprints, then run `pnpm pjt remodel`.",
99
+ )
100
+ yield* display(`${lines.join("\n")}\n`)
101
+ }),
102
+ ).pipe(Command.withDescription("Bootstrap projitect in the current project."))
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // pjt remodel
106
+ // ---------------------------------------------------------------------------
107
+
108
+ const remodelCmd = Command.make(
109
+ "remodel",
110
+ {
111
+ json: jsonFlag("Emit the result, or the error with its id and fields, as JSON."),
112
+ },
113
+ (input) =>
114
+ Effect.gen(function* () {
115
+ const config = configFromEnv()
116
+ const result = yield* remodel({ config }).pipe(
117
+ Effect.matchEffect({
118
+ onSuccess: (r) => Effect.succeed(r),
119
+ onFailure: (error: Errors.ProjitectError) =>
120
+ reportError(error, input.json).pipe(Effect.as(null)),
121
+ }),
122
+ )
123
+ if (result === null) {
124
+ return
125
+ }
126
+ if (input.json) {
127
+ yield* display(`${JSON.stringify(result, null, 2)}\n`)
128
+ return
129
+ }
130
+ const parts: string[] = []
131
+ if (result.written.length > 0) {
132
+ parts.push(
133
+ `Wrote ${result.written.length} file${result.written.length === 1 ? "" : "s"}:`,
134
+ ...result.written.map((p) => ` ${p}`),
135
+ )
136
+ }
137
+ if (result.removed.length > 0) {
138
+ parts.push(
139
+ `Cleaned up ${result.removed.length} orphan${result.removed.length === 1 ? "" : "s"}:`,
140
+ ...result.removed.map((p) => ` ${p}`),
141
+ )
142
+ }
143
+ if (result.ran.length > 0) {
144
+ parts.push(
145
+ `Ran ${result.ran.length} command${result.ran.length === 1 ? "" : "s"}:`,
146
+ ...result.ran.map((c) => ` ${c}`),
147
+ )
148
+ }
149
+ if (parts.length === 0) {
150
+ parts.push("Project already in sync. No changes written.")
151
+ }
152
+ yield* display(`${parts.join("\n")}\n`)
153
+ }),
154
+ ).pipe(
155
+ Command.withDescription(
156
+ "Apply the blueprint plan to disk (non-destructive), then run post-apply commands.",
157
+ ),
158
+ )
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // pjt inspect
162
+ // ---------------------------------------------------------------------------
163
+
164
+ const inspectCmd = Command.make(
165
+ "inspect",
166
+ {
167
+ json: Flag.boolean("json").pipe(
168
+ Flag.withDefault(false),
169
+ Flag.withDescription(
170
+ `Emit machine-readable JSON output conforming to ${InspectJsonSchemaUrl}; an error is emitted as an \`{ "error": ... }\` object. Exit code still 1 on drift.`,
171
+ ),
172
+ ),
173
+ },
174
+ (input) =>
175
+ Effect.gen(function* () {
176
+ const config = configFromEnv()
177
+ const result = yield* inspect({ config }).pipe(
178
+ Effect.matchEffect({
179
+ onSuccess: (r) => Effect.succeed(r),
180
+ onFailure: (error: Errors.ProjitectError) =>
181
+ reportError(error, input.json).pipe(Effect.as(null)),
182
+ }),
183
+ )
184
+ if (result === null) {
185
+ return
186
+ }
187
+ const rendered = input.json ? renderInspectJson(result) : `${result.output}\n`
188
+ yield* display(rendered)
189
+ if (result.hasDrift) {
190
+ process.exitCode = 1
191
+ }
192
+ }),
193
+ ).pipe(
194
+ Command.withDescription(
195
+ "Report drift between project and blueprints (exit 1 on drift; built for CI).",
196
+ ),
197
+ )
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // pjt build --force
201
+ // ---------------------------------------------------------------------------
202
+
203
+ const buildCmd = Command.make(
204
+ "build",
205
+ {
206
+ force: Flag.boolean("force").pipe(
207
+ Flag.withDefault(false),
208
+ Flag.withDescription("Required. Wipe the project tree and rebuild from scratch."),
209
+ ),
210
+ forceDirty: Flag.boolean("force-dirty").pipe(
211
+ Flag.withDefault(false),
212
+ Flag.withDescription("Allow wiping even when git has uncommitted changes."),
213
+ ),
214
+ yes: Flag.boolean("yes").pipe(
215
+ Flag.withDefault(false),
216
+ Flag.withDescription("Skip the interactive confirmation prompt."),
217
+ ),
218
+ json: jsonFlag("Emit the result, or the error with its id and fields, as JSON."),
219
+ },
220
+ (input) =>
221
+ Effect.gen(function* () {
222
+ const config = configFromEnv()
223
+ const result = yield* build({
224
+ config,
225
+ force: input.force,
226
+ forceDirty: input.forceDirty,
227
+ yes: input.yes,
228
+ }).pipe(
229
+ Effect.matchEffect({
230
+ onSuccess: (r) => Effect.succeed(r),
231
+ onFailure: (error) =>
232
+ Match.value(error).pipe(
233
+ // QuitError from Prompt = user pressed Ctrl-C; treat as graceful cancel.
234
+ Match.tag("QuitError", () => {
235
+ process.exitCode = 130
236
+ return display("Cancelled.\n").pipe(Effect.as(null))
237
+ }),
238
+ Match.orElse((projitectError) =>
239
+ reportError(projitectError, input.json).pipe(Effect.as(null)),
240
+ ),
241
+ ),
242
+ }),
243
+ )
244
+ if (result === null) {
245
+ return
246
+ }
247
+ if (input.json) {
248
+ yield* display(`${JSON.stringify(result, null, 2)}\n`)
249
+ return
250
+ }
251
+ yield* display(renderBuildResult(result))
252
+ }),
253
+ ).pipe(
254
+ Command.withDescription(
255
+ "Scratch-build the project from blueprints. Destructive — requires --force and clean git.",
256
+ ),
257
+ )
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // pjt explain <error-id>
261
+ // ---------------------------------------------------------------------------
262
+
263
+ const explainCmd = Command.make("explain", { errorId: Argument.string("error-id") }, (input) =>
264
+ Effect.gen(function* () {
265
+ const out = yield* explain({ errorId: input.errorId })
266
+ yield* display(`${out}\n`)
267
+ }),
268
+ ).pipe(Command.withDescription("Print a description of a projitect error id."))
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // pjt add <package>
272
+ // ---------------------------------------------------------------------------
273
+
274
+ const addCmd = Command.make(
275
+ "add",
276
+ {
277
+ pkg: Argument.string("package"),
278
+ section: Flag.string("section").pipe(
279
+ Flag.optional,
280
+ Flag.withDescription(
281
+ "Comma-separated section names for blueprint-set packages (e.g. `--section macOs,node`).",
282
+ ),
283
+ ),
284
+ },
285
+ (input) =>
286
+ Effect.gen(function* () {
287
+ const config = configFromEnv()
288
+ const explicitSections = Option.match(input.section, {
289
+ onNone: () => null as readonly string[] | null,
290
+ onSome: (raw) =>
291
+ raw
292
+ .split(",")
293
+ .map((s) => s.trim())
294
+ .filter((s) => s.length > 0),
295
+ })
296
+
297
+ // When `--section` is given, honor it verbatim. Otherwise: if stdin is a TTY, ask the
298
+ // user via `Prompt.multiSelect`; if not (CI / piped), fall back to "all" so scripted
299
+ // installs stay deterministic. The strategy is parametrized over Prompt.Environment so
300
+ // the requirement propagates out through `add`; Command.run provides the platform.
301
+ const strategy: SectionStrategy<Prompt.Environment> =
302
+ explicitSections === null
303
+ ? process.stdin.isTTY
304
+ ? { _tag: "Ask", choose: chooseSectionsInteractive }
305
+ : { _tag: "All" }
306
+ : { _tag: "Explicit", sections: explicitSections }
307
+
308
+ const result = yield* add({ config, pkg: input.pkg, strategy }).pipe(
309
+ Effect.matchEffect({
310
+ onSuccess: (r) => Effect.succeed(r),
311
+ onFailure: (error: Errors.ProjitectError) => reportError(error).pipe(Effect.as(null)),
312
+ }),
313
+ )
314
+ if (result === null) {
315
+ return
316
+ }
317
+ const lines = [`Installed ${result.pkg} via ${result.pm}.`]
318
+ if (result.splicedIntoBlueprintFile) {
319
+ if (result.sectionsAdded.length > 0) {
320
+ lines.push(`Added to ${config.blueprintFile}: ${result.sectionsAdded.join(", ")}.`)
321
+ } else {
322
+ lines.push(`Added to ${config.blueprintFile}.`)
323
+ }
324
+ lines.push("Run `pjt remodel` to apply.")
325
+ } else {
326
+ lines.push(`No sections selected; ${config.blueprintFile} was not changed.`)
327
+ }
328
+ yield* display(`${lines.join("\n")}\n`)
329
+ }),
330
+ ).pipe(
331
+ Command.withDescription(
332
+ "Install a blueprint package and splice it into .pjt.ts via the marker anchors.",
333
+ ),
334
+ )
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // Root
338
+ // ---------------------------------------------------------------------------
339
+
340
+ export const rootCommand = Command.make("pjt").pipe(
341
+ Command.withDescription("Project scaffolding that stays in sync — drift-aware blueprints."),
342
+ Command.withSubcommands([initCmd, remodelCmd, inspectCmd, buildCmd, explainCmd, addCmd]),
343
+ )
344
+
345
+ export type RootCommand = typeof rootCommand
@@ -0,0 +1,81 @@
1
+ import { Effect, Option, Schema } from "effect"
2
+ import { Errors } from "@projitect/core"
3
+
4
+ const ErrorId = Schema.Literals(Errors.ERROR_IDS)
5
+
6
+ /**
7
+ * `pjt explain <id>` — print a short description of an error id, with a link to the docs page.
8
+ * The full content lives on the marketing site at `/errors/<id>`; this CLI gives the basics
9
+ * without a network call.
10
+ */
11
+ export const explain = (params: { readonly errorId: string }): Effect.Effect<string> =>
12
+ Effect.sync(() =>
13
+ Option.match(Schema.decodeUnknownOption(ErrorId)(params.errorId), {
14
+ onNone: () =>
15
+ `Unknown error id: ${params.errorId}\nSee https://projitect.dev/errors for the full registry.`,
16
+ onSome: (id) => `${id}\n${ERROR_BLURBS[id]}\nMore at: ${Errors.docsUrl(id)}`,
17
+ }),
18
+ )
19
+
20
+ const ERROR_BLURBS: Readonly<Record<Errors.ErrorId, string>> = {
21
+ "pjt.fs.permission-denied":
22
+ "A blueprint attempted a filesystem operation outside its declared permissions. Check the blueprint's `permissions` array.",
23
+ "pjt.fs.read-failed":
24
+ "A read operation failed at the OS level (typically ENOENT or EACCES). The cause is printed alongside.",
25
+ "pjt.fs.write-failed":
26
+ "A write operation failed at the OS level. The cause is printed alongside.",
27
+ "pjt.loader.import-failed":
28
+ "The `.pjt.ts` file could not be imported. Check for syntax errors or missing dependencies.",
29
+ "pjt.loader.invalid-default-export":
30
+ "`.pjt.ts` must `export default pjt({...})`. Found a different shape.",
31
+ "pjt.config.invalid":
32
+ "A config value failed schema validation. The offending field and source layer are reported.",
33
+ "pjt.config.blueprint-file-not-found":
34
+ "No `.pjt.ts` at the project root. Run `pjt init` to create one.",
35
+ "pjt.plan.conflict-region":
36
+ "Two blueprints share the same `ownerId` for a region in the same file. Pick distinct ids.",
37
+ "pjt.plan.conflict-merge":
38
+ "Two blueprints claim the same JSON key in the same file. Coordinate ownership.",
39
+ "pjt.plan.conflict-owned":
40
+ "Two blueprints both claim full ownership of one file. Only one `ownFile` per path is allowed.",
41
+ "pjt.skills.invalid-path":
42
+ "A skill name, target root or file path is empty, absolute, or tries to leave the skills directory — or a skill has no `SKILL.md`.",
43
+ "pjt.region.missing-end":
44
+ "A region start marker was found without a matching end marker. The file has been hand-edited and the closing marker removed.",
45
+ "pjt.region.duplicate": "Multiple start markers for the same region in one file. Hand-edited?",
46
+ "pjt.apply.dirty-git":
47
+ "`build` requires a clean git working tree. Commit or stash first, then retry.",
48
+ "pjt.init.git-missing":
49
+ "`pjt init` requires a git repository. Run `git init` first, or rerun with --yes.",
50
+ "pjt.init.package-json-missing":
51
+ "`pjt init` requires a `package.json`. Run `npm init -y` first, or rerun with --yes.",
52
+ "pjt.drift.detected":
53
+ "`pjt inspect` exits nonzero when the project drifts from its blueprints. Run `pjt remodel` to sync.",
54
+ "pjt.lock.parse-failed":
55
+ "`.pjt.lock` is corrupted or doesn't match the expected schema. Delete it and rerun `pjt remodel` to rebuild.",
56
+ "pjt.lock.version-mismatch":
57
+ "`.pjt.lock` was written by a newer projitect than the one currently installed. Upgrade the `projitect` devDep.",
58
+ "pjt.git.not-a-repo": "Required a git repository, but `.git/` was missing. Run `git init` first.",
59
+ "pjt.git.command-failed":
60
+ "A `git` command exited non-zero or git isn't installed. The underlying error is included in the message.",
61
+ "pjt.exec.command-failed":
62
+ "A command run by projitect exited non-zero or could not be started. Its exit code, stdout and stderr are included.",
63
+ "pjt.exec.command-not-permitted":
64
+ "A blueprint tried to run a command that none of its declared `exec` permissions names exactly.",
65
+ "pjt.inspect.operation-unsupported":
66
+ "`pjt inspect` cannot check a project whose blueprints declare post-apply commands, so it fails instead of reporting clean.",
67
+ "pjt.pm.not-detected":
68
+ "Could not figure out which package manager you're using. Add a lockfile (`pnpm-lock.yaml`, `package-lock.json`, etc.) at your project root.",
69
+ "pjt.pm.install-failed":
70
+ "The package manager install command exited non-zero. The underlying error is included in the message.",
71
+ "pjt.add.markers-missing":
72
+ "Your `.pjt.ts` is missing the marker comment pairs `pjt:imports` and `pjt:blueprints`. `pjt add` uses those as splice anchors. Restore them and re-run.",
73
+ "pjt.add.metadata-missing":
74
+ "The package you added has no `projitect` field in its package.json, so `pjt add` can't splice it. Import it in `.pjt.ts` by hand, then run `pjt remodel`.",
75
+ "pjt.worker.crashed":
76
+ "The worker process that runs blueprint code crashed before it returned a result. The exit code or signal is included in the message.",
77
+ "pjt.worker.timed-out":
78
+ "The worker process that runs blueprint code did not finish within its time limit and was stopped. Check for a blueprint that never completes.",
79
+ "pjt.worker.exited-early":
80
+ "The worker process that runs blueprint code exited before it finished its work. The exit code or signal is included in the message.",
81
+ }
@@ -0,0 +1,233 @@
1
+ import { promises as fs } from "node:fs"
2
+ import { spawn } from "node:child_process"
3
+ import { createRequire } from "node:module"
4
+ import path from "node:path"
5
+ import { Array, Effect, Option, Schema } from "effect"
6
+ import { Errors } from "@projitect/core"
7
+ import type { ProjitectConfig } from "@projitect/core"
8
+ import { detect, installDev as installDevelopment, installProject } from "../pm.js"
9
+ import { remodel } from "./remodel.js"
10
+ import type { RemodelResult } from "./remodel.js"
11
+
12
+ const manifest = Schema.decodeUnknownSync(
13
+ Schema.Struct({
14
+ version: Schema.String,
15
+ peerDependencies: Schema.Struct({ effect: Schema.String }),
16
+ }),
17
+ )(createRequire(import.meta.url)("../../package.json"))
18
+
19
+ export interface InitResult {
20
+ readonly seededBlueprintFile: boolean
21
+ readonly bootstrappedGit: boolean
22
+ readonly bootstrappedPackageJson: boolean
23
+ readonly remodel: RemodelResult
24
+ }
25
+
26
+ /**
27
+ * `pjt init` — bootstrap projitect in the current project.
28
+ *
29
+ * Verifies the prerequisites (`.git/` and `package.json` both present), seeds an empty
30
+ * `.pjt.ts` with the marker structure if absent, then delegates to `remodel` which runs the
31
+ * standard plan/apply/lockfile-write pipeline. The projitect blueprint (prepended automatically
32
+ * by `@projitect/cli`'s `pjt()`) writes the package.json entries and the `.pjt.ts` import region
33
+ * on its first apply. Before seeding or loading the config, missing local CLI and Effect
34
+ * dependencies are installed so an ephemeral dlx invocation can resolve those imports.
35
+ *
36
+ * With `yes: true`, both prerequisites are auto-bootstrapped instead of erroring:
37
+ * - Missing `.git/` → `git init -q` shell-out.
38
+ * - Missing `package.json` → a minimal stub is written (`{ "name": "<dirname>", ... }`).
39
+ * This is what CI / scripted setups use (`pjt init --yes`); the interactive flow keeps the
40
+ * stricter prerequisite checks so a typo doesn't accidentally create a git repo in `$HOME`.
41
+ *
42
+ * No special-case code for "the projitect bootstrap" lives here — the projitect blueprint owns
43
+ * those concerns and runs through the same pipeline as every other blueprint.
44
+ */
45
+ export const init = (params: {
46
+ readonly config: ProjitectConfig.ProjitectConfig
47
+ readonly yes?: boolean
48
+ }): Effect.Effect<InitResult, Errors.ProjitectError> =>
49
+ Effect.gen(function* () {
50
+ const bootstrappedGit = yield* ensureGit(params.config.projectRoot, params.yes ?? false)
51
+ const bootstrappedPackageJson = yield* ensurePackageJson(
52
+ params.config.projectRoot,
53
+ params.yes ?? false,
54
+ )
55
+
56
+ const bootstrapPm = yield* ensureBootstrapDependencies(params.config.projectRoot)
57
+
58
+ const seededBlueprintFile = yield* seedBlueprintFileIfAbsent(
59
+ params.config.projectRoot,
60
+ params.config.blueprintFile,
61
+ )
62
+
63
+ const remodelResult = yield* remodel({ config: params.config })
64
+ yield* Option.match(bootstrapPm, {
65
+ onNone: () => Effect.void,
66
+ onSome: (pm) => installProject({ projectRoot: params.config.projectRoot, pm }),
67
+ })
68
+
69
+ return {
70
+ seededBlueprintFile,
71
+ bootstrappedGit,
72
+ bootstrappedPackageJson,
73
+ remodel: remodelResult,
74
+ }
75
+ })
76
+
77
+ /** A dlx runner lives outside the project; generated imports must resolve locally first. */
78
+ const ensureBootstrapDependencies = (projectRoot: string) =>
79
+ Effect.forEach(
80
+ [
81
+ { name: "@projitect/cli", spec: `@projitect/cli@${manifest.version}` },
82
+ { name: "effect", spec: `effect@${manifest.peerDependencies.effect}` },
83
+ ],
84
+ ({ name, spec }) =>
85
+ Effect.gen(function* () {
86
+ const installed = yield* Effect.promise(() =>
87
+ fs.access(path.join(projectRoot, "node_modules", name, "package.json")).then(
88
+ () => true,
89
+ () => false,
90
+ ),
91
+ )
92
+ if (installed) {
93
+ return Option.none()
94
+ }
95
+ const pm = yield* detect({ projectRoot })
96
+ yield* installDevelopment({ projectRoot, pm, pkg: spec })
97
+ return Option.some(pm)
98
+ }),
99
+ ).pipe(Effect.map((results) => Array.head(Array.getSomes(results))))
100
+
101
+ const ensureGit = (
102
+ projectRoot: string,
103
+ yes: boolean,
104
+ ): Effect.Effect<boolean, Errors.InitGitMissing> =>
105
+ Effect.gen(function* () {
106
+ const ok = yield* Effect.promise(() =>
107
+ fs.access(path.join(projectRoot, ".git")).then(
108
+ () => true,
109
+ () => false,
110
+ ),
111
+ )
112
+ if (ok) {
113
+ return false
114
+ }
115
+ if (!yes) {
116
+ return yield* new Errors.InitGitMissing({
117
+ id: "pjt.init.git-missing",
118
+ message:
119
+ "No `.git` directory found. Run `git init` and then `pjt init` again, or run `pjt init --yes` to create the repository for you.",
120
+ })
121
+ }
122
+ yield* Effect.promise(
123
+ () =>
124
+ new Promise<void>((resolve, reject) => {
125
+ const child = spawn("git", ["init", "-q"], { cwd: projectRoot, stdio: "ignore" })
126
+ child.on("error", reject)
127
+ child.on("close", (code) => {
128
+ if (code === 0) {
129
+ resolve()
130
+ } else {
131
+ reject(new Error(`git init exited with ${String(code)}`))
132
+ }
133
+ })
134
+ }),
135
+ )
136
+ return true
137
+ })
138
+
139
+ const ensurePackageJson = (
140
+ projectRoot: string,
141
+ yes: boolean,
142
+ ): Effect.Effect<boolean, Errors.InitPackageJsonMissing | Errors.FsWriteFailed> =>
143
+ Effect.gen(function* () {
144
+ const ok = yield* Effect.promise(() =>
145
+ fs.access(path.join(projectRoot, "package.json")).then(
146
+ () => true,
147
+ () => false,
148
+ ),
149
+ )
150
+ if (ok) {
151
+ return false
152
+ }
153
+ if (!yes) {
154
+ return yield* new Errors.InitPackageJsonMissing({
155
+ id: "pjt.init.package-json-missing",
156
+ message:
157
+ "No `package.json` found. Run `npm init -y` (or your PM equivalent) first, then re-run `pjt init` — or pass `--yes` to auto-bootstrap.",
158
+ })
159
+ }
160
+ const name = path.basename(projectRoot) || "project"
161
+ const stub = {
162
+ name,
163
+ version: "0.0.0",
164
+ private: true,
165
+ type: "module" as const,
166
+ }
167
+ yield* Effect.tryPromise({
168
+ try: () =>
169
+ fs.writeFile(
170
+ path.join(projectRoot, "package.json"),
171
+ `${JSON.stringify(stub, null, 2)}\n`,
172
+ "utf8",
173
+ ),
174
+ catch: (e) =>
175
+ new Errors.FsWriteFailed({
176
+ id: "pjt.fs.write-failed",
177
+ path: "package.json",
178
+ cause: e instanceof Error ? e.message : String(e),
179
+ message: "Failed to seed package.json during `pjt init --yes`",
180
+ }),
181
+ })
182
+ return true
183
+ })
184
+
185
+ const seedBlueprintFileIfAbsent = (
186
+ projectRoot: string,
187
+ blueprintFile: string,
188
+ ): Effect.Effect<boolean, Errors.FsWriteFailed> =>
189
+ Effect.gen(function* () {
190
+ const full = path.join(projectRoot, blueprintFile)
191
+ const exists = yield* Effect.promise(() =>
192
+ fs.access(full).then(
193
+ () => true,
194
+ () => false,
195
+ ),
196
+ )
197
+ if (exists) {
198
+ return false
199
+ }
200
+
201
+ yield* Effect.tryPromise({
202
+ try: () => fs.writeFile(full, STARTER_PJT_TS, "utf8"),
203
+ catch: (e) =>
204
+ new Errors.FsWriteFailed({
205
+ id: "pjt.fs.write-failed",
206
+ path: blueprintFile,
207
+ cause: e instanceof Error ? e.message : String(e),
208
+ message: `Failed to seed ${blueprintFile}`,
209
+ }),
210
+ })
211
+ return true
212
+ })
213
+
214
+ /**
215
+ * Starter `.pjt.ts` template. Three marker regions: the projitect blueprint owns
216
+ * `pjt:projitect:imports` (it writes the `import { pjt } from "@projitect/cli"` line on first
217
+ * remodel). The other two — `pjt:imports` and `pjt:blueprints` — are **convention anchors**
218
+ * that `pjt add` splices into; they are not managed regions and don't appear in `.pjt.lock`.
219
+ */
220
+ const STARTER_PJT_TS = `// pjt:projitect:imports start
221
+ import { pjt } from "@projitect/cli"
222
+ // pjt:projitect:imports end
223
+
224
+ // pjt:imports start
225
+ // pjt:imports end
226
+
227
+ export default pjt({
228
+ blueprints: [
229
+ // pjt:blueprints start
230
+ // pjt:blueprints end
231
+ ],
232
+ })
233
+ `