create-warlock 4.16.0 → 5.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 (38) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/esm/commands/create-new-app/index.mjs +14 -4
  3. package/esm/commands/create-new-app/index.mjs.map +1 -1
  4. package/esm/commands/create-warlock-app/index.mjs +123 -16
  5. package/esm/commands/create-warlock-app/index.mjs.map +1 -1
  6. package/esm/features/features-map.mjs +6 -0
  7. package/esm/features/features-map.mjs.map +1 -1
  8. package/esm/helpers/app.mjs +74 -3
  9. package/esm/helpers/app.mjs.map +1 -1
  10. package/esm/helpers/exec.mjs +164 -23
  11. package/esm/helpers/exec.mjs.map +1 -1
  12. package/esm/helpers/project-builder-helpers.mjs +22 -12
  13. package/esm/helpers/project-builder-helpers.mjs.map +1 -1
  14. package/esm/helpers/warlock-versions.mjs +166 -0
  15. package/esm/helpers/warlock-versions.mjs.map +1 -0
  16. package/esm/index.mjs +7 -1
  17. package/esm/index.mjs.map +1 -1
  18. package/esm/ui/report.mjs +98 -0
  19. package/esm/ui/report.mjs.map +1 -0
  20. package/esm/ui/spinners.mjs +13 -3
  21. package/esm/ui/spinners.mjs.map +1 -1
  22. package/package.json +2 -2
  23. package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +2 -5
  24. package/templates/warlock/src/app/auth/controllers/login.controller.ts +4 -1
  25. package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +2 -2
  26. package/templates/warlock/src/app/auth/controllers/logout.controller.ts +2 -2
  27. package/templates/warlock/src/app/auth/controllers/me.controller.ts +2 -2
  28. package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +2 -5
  29. package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +2 -2
  30. package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +2 -2
  31. package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +2 -2
  32. package/templates/warlock/src/app/shared/controllers/home-page.controller.ts +1 -1
  33. package/templates/warlock/src/app/shared/controllers/home-page.controller.tsx +2 -2
  34. package/templates/warlock/src/app/uploads/controllers/fetch-uploaded-file.controller.ts +1 -1
  35. package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +2 -2
  36. package/templates/warlock/src/app/users/controllers/list-users.controller.ts +1 -1
  37. package/templates/warlock/src/app/users/services/login-social.ts +13 -3
  38. package/templates/warlock/src/config/cache.ts +4 -1
@@ -4,44 +4,184 @@ import childProcess from "cross-spawn";
4
4
 
5
5
  //#region ../create-warlock/src/helpers/exec.ts
6
6
  /**
7
+ * The last command executed through {@link executeCommand}.
8
+ *
9
+ * `executeCommand` resolves a bare boolean (a contract several callers depend
10
+ * on), so the *reason* a command failed would otherwise be lost. It is parked
11
+ * here and claimed with {@link takeLastCommandOutput}, which clears it — a
12
+ * caller can never accidentally attribute a stale failure to the wrong step.
13
+ *
14
+ * The scaffolder runs one command at a time, so a single slot is enough.
15
+ */
16
+ let lastCommandOutput;
17
+ /**
18
+ * Claim (and clear) the output of the most recent {@link executeCommand} call.
19
+ */
20
+ function takeLastCommandOutput() {
21
+ const output = lastCommandOutput;
22
+ lastCommandOutput = void 0;
23
+ return output;
24
+ }
25
+ /**
26
+ * Keep only the last `lines` non-empty lines of a stream — enough context to
27
+ * act on, without dumping a 500-line npm log over the wizard.
28
+ */
29
+ function tail(text, lines = 12) {
30
+ return text.split(/\r?\n/).filter((line) => line.trim().length > 0).slice(-lines).join("\n");
31
+ }
32
+ /**
33
+ * Attach stdout/stderr collectors to a child and return the accumulated text.
34
+ *
35
+ * The streams are optional on purpose: a child spawned with `stdio: "ignore"`
36
+ * (or a test double) exposes none.
37
+ */
38
+ function collectOutput(child) {
39
+ const chunks = {
40
+ stdout: "",
41
+ stderr: ""
42
+ };
43
+ child.stdout?.on("data", (data) => {
44
+ chunks.stdout += String(data);
45
+ });
46
+ child.stderr?.on("data", (data) => {
47
+ chunks.stderr += String(data);
48
+ });
49
+ return chunks;
50
+ }
51
+ function logSpawnError(error) {
52
+ if (!error) return;
53
+ const message = error?.message;
54
+ log.error(colors.red(String(message ?? error)) + `\n\n`);
55
+ }
56
+ /**
7
57
  * This function directly executes a command
58
+ *
59
+ * Resolves a boolean for backwards compatibility; the full outcome (exit code
60
+ * and captured output) is available to the caller via
61
+ * {@link takeLastCommandOutput}.
8
62
  */
9
63
  async function executeCommand(cmd, args, cwd) {
64
+ const result = await runCapturedCommand(cmd, args, cwd);
65
+ lastCommandOutput = result;
66
+ return result.ok;
67
+ }
68
+ /**
69
+ * Run a command to completion and resolve its full {@link CommandResult}.
70
+ *
71
+ * Output is piped and captured rather than discarded — the whole point is that
72
+ * a failure can be explained instead of merely announced.
73
+ */
74
+ function runCapturedCommand(cmd, args, cwd, options = {}) {
10
75
  return new Promise((resolve) => {
11
- const child = childProcess(cmd, args, {
76
+ const command = [cmd, ...args].join(" ");
77
+ const settle = (result) => resolve({
78
+ ok: false,
79
+ command,
12
80
  cwd,
13
- stdio: "ignore"
81
+ code: null,
82
+ signal: null,
83
+ stdout: "",
84
+ stderr: "",
85
+ ...result
14
86
  });
15
- child.on("error", (e) => {
16
- if (e) if (e.message) log.error(colors.red(String(e.message)) + `\n\n`);
17
- else log.error(colors.red(String(e)) + `\n\n`);
18
- resolve(false);
87
+ let child;
88
+ try {
89
+ child = childProcess(cmd, args, {
90
+ cwd,
91
+ stdio: [
92
+ "ignore",
93
+ "pipe",
94
+ "pipe"
95
+ ],
96
+ env: options.env ? {
97
+ ...process.env,
98
+ ...options.env
99
+ } : process.env
100
+ });
101
+ } catch (error) {
102
+ logSpawnError(error);
103
+ return settle({
104
+ error,
105
+ stderr: String(error)
106
+ });
107
+ }
108
+ const output = collectOutput(child);
109
+ child.on("error", (error) => {
110
+ logSpawnError(error);
111
+ settle({
112
+ error,
113
+ stdout: output.stdout,
114
+ stderr: output.stderr || String(error?.message ?? error)
115
+ });
19
116
  });
20
- child.on("close", (code) => {
21
- if (code === 0) resolve(true);
22
- else resolve(false);
117
+ child.on("close", (code, signal) => {
118
+ settle({
119
+ ok: code === 0,
120
+ code,
121
+ signal,
122
+ stdout: output.stdout,
123
+ stderr: output.stderr
124
+ });
23
125
  });
24
126
  });
25
127
  }
26
- function runCommand(cmd, args, cwd) {
128
+ /**
129
+ * Run a long command with an abort handle.
130
+ *
131
+ * `install` stays a `Promise<boolean>` for existing callers; `result` carries
132
+ * the exit code and the captured output so a failing install can be reported
133
+ * with the command, its status and its stderr instead of a shrug.
134
+ */
135
+ function runCommand(cmd, args, cwd, options = {}) {
27
136
  let child;
28
- const install = new Promise((resolve) => {
137
+ const result = new Promise((resolve) => {
138
+ const command = [cmd, ...args].join(" ");
139
+ const settle = (partial) => resolve({
140
+ ok: false,
141
+ command,
142
+ cwd,
143
+ code: null,
144
+ signal: null,
145
+ stdout: "",
146
+ stderr: "",
147
+ ...partial
148
+ });
29
149
  try {
30
150
  child = childProcess(cmd, args, {
31
151
  cwd,
32
- stdio: "ignore"
152
+ stdio: [
153
+ "ignore",
154
+ "pipe",
155
+ "pipe"
156
+ ],
157
+ env: options.env ? {
158
+ ...process.env,
159
+ ...options.env
160
+ } : process.env
161
+ });
162
+ const output = collectOutput(child);
163
+ child.on("error", (error) => {
164
+ logSpawnError(error);
165
+ settle({
166
+ error,
167
+ stdout: output.stdout,
168
+ stderr: output.stderr || String(error?.message ?? error)
169
+ });
33
170
  });
34
- child.on("error", (e) => {
35
- if (e) if (e.message) log.error(colors.red(String(e.message)) + `\n\n`);
36
- else log.error(colors.red(String(e)) + `\n\n`);
37
- resolve(false);
171
+ child.on("close", (code, signal) => {
172
+ settle({
173
+ ok: code === 0,
174
+ code,
175
+ signal,
176
+ stdout: output.stdout,
177
+ stderr: output.stderr
178
+ });
38
179
  });
39
- child.on("close", (code) => {
40
- if (code === 0) resolve(true);
41
- else resolve(false);
180
+ } catch (error) {
181
+ settle({
182
+ error,
183
+ stderr: String(error)
42
184
  });
43
- } catch (e) {
44
- resolve(false);
45
185
  }
46
186
  });
47
187
  const abort = async () => {
@@ -49,10 +189,11 @@ function runCommand(cmd, args, cwd) {
49
189
  };
50
190
  return {
51
191
  abort,
52
- install
192
+ install: result.then((outcome) => outcome.ok),
193
+ result
53
194
  };
54
195
  }
55
196
 
56
197
  //#endregion
57
- export { executeCommand, runCommand };
198
+ export { executeCommand, runCommand, tail, takeLastCommandOutput };
58
199
  //# sourceMappingURL=exec.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"exec.mjs","names":["spawn"],"sources":["../../../../../../create-warlock/src/helpers/exec.ts"],"sourcesContent":["import { log } from \"@clack/prompts\";\r\nimport { colors } from \"@mongez/copper\";\r\nimport { ChildProcess } from \"child_process\";\r\nimport { default as childProcess, default as spawn } from \"cross-spawn\";\r\n\r\nexport default async function exec(command: string, options: any = {}) {\r\n const [commandName, ...optionsList] = command.split(\" \");\r\n\r\n const commandOutput = childProcess.sync(commandName, optionsList, options);\r\n\r\n // it means command didn't end as expected, then stop the rest of the program\r\n if (commandOutput.error !== null) {\r\n process.exit(1);\r\n }\r\n\r\n return commandOutput;\r\n}\r\n\r\n/**\r\n * This function directly executes a command\r\n */\r\nexport async function executeCommand(cmd: string, args: string[], cwd: string) {\r\n return new Promise<boolean>(resolve => {\r\n const child = spawn(cmd, args, {\r\n cwd,\r\n stdio: \"ignore\",\r\n });\r\n\r\n child.on(\"error\", e => {\r\n if (e) {\r\n if (e.message) {\r\n log.error(colors.red(String(e.message)) + `\\n\\n`);\r\n } else {\r\n log.error(colors.red(String(e)) + `\\n\\n`);\r\n }\r\n }\r\n resolve(false);\r\n });\r\n\r\n child.on(\"close\", code => {\r\n if (code === 0) {\r\n resolve(true);\r\n } else {\r\n resolve(false);\r\n }\r\n });\r\n });\r\n}\r\n\r\nexport function runCommand(cmd: string, args: string[], cwd: string) {\r\n let child: ChildProcess;\r\n\r\n const install = new Promise<boolean>(resolve => {\r\n try {\r\n child = spawn(cmd, args, {\r\n cwd,\r\n stdio: \"ignore\",\r\n });\r\n\r\n child.on(\"error\", e => {\r\n if (e) {\r\n if (e.message) {\r\n log.error(colors.red(String(e.message)) + `\\n\\n`);\r\n } else {\r\n log.error(colors.red(String(e)) + `\\n\\n`);\r\n }\r\n }\r\n resolve(false);\r\n });\r\n\r\n child.on(\"close\", code => {\r\n if (code === 0) {\r\n resolve(true);\r\n } else {\r\n resolve(false);\r\n }\r\n });\r\n } catch (e) {\r\n resolve(false);\r\n }\r\n });\r\n\r\n const abort = async () => {\r\n if (child) {\r\n child.kill(\"SIGINT\");\r\n }\r\n };\r\n\r\n return { abort, install };\r\n}\r\n"],"mappings":";;;;;;;;AAqBA,eAAsB,eAAe,KAAa,MAAgB,KAAa;CAC7E,OAAO,IAAI,SAAiB,YAAW;EACrC,MAAM,QAAQA,aAAM,KAAK,MAAM;GAC7B;GACA,OAAO;EACT,CAAC;EAED,MAAM,GAAG,UAAS,MAAK;GACrB,IAAI,GACF,IAAI,EAAE,SACJ,IAAI,MAAM,OAAO,IAAI,OAAO,EAAE,OAAO,CAAC,IAAI,MAAM;QAEhD,IAAI,MAAM,OAAO,IAAI,OAAO,CAAC,CAAC,IAAI,MAAM;GAG5C,QAAQ,KAAK;EACf,CAAC;EAED,MAAM,GAAG,UAAS,SAAQ;GACxB,IAAI,SAAS,GACX,QAAQ,IAAI;QAEZ,QAAQ,KAAK;EAEjB,CAAC;CACH,CAAC;AACH;AAEA,SAAgB,WAAW,KAAa,MAAgB,KAAa;CACnE,IAAI;CAEJ,MAAM,UAAU,IAAI,SAAiB,YAAW;EAC9C,IAAI;GACF,QAAQA,aAAM,KAAK,MAAM;IACvB;IACA,OAAO;GACT,CAAC;GAED,MAAM,GAAG,UAAS,MAAK;IACrB,IAAI,GACF,IAAI,EAAE,SACJ,IAAI,MAAM,OAAO,IAAI,OAAO,EAAE,OAAO,CAAC,IAAI,MAAM;SAEhD,IAAI,MAAM,OAAO,IAAI,OAAO,CAAC,CAAC,IAAI,MAAM;IAG5C,QAAQ,KAAK;GACf,CAAC;GAED,MAAM,GAAG,UAAS,SAAQ;IACxB,IAAI,SAAS,GACX,QAAQ,IAAI;SAEZ,QAAQ,KAAK;GAEjB,CAAC;EACH,SAAS,GAAG;GACV,QAAQ,KAAK;EACf;CACF,CAAC;CAED,MAAM,QAAQ,YAAY;EACxB,IAAI,OACF,MAAM,KAAK,QAAQ;CAEvB;CAEA,OAAO;EAAE;EAAO;CAAQ;AAC1B"}
1
+ {"version":3,"file":"exec.mjs","names":["spawn"],"sources":["../../../../../../create-warlock/src/helpers/exec.ts"],"sourcesContent":["import { log } from \"@clack/prompts\";\nimport { colors } from \"@mongez/copper\";\nimport { ChildProcess } from \"child_process\";\nimport { default as childProcess, default as spawn } from \"cross-spawn\";\n\n/**\n * The full outcome of a spawned command — everything a human needs to act on a\n * failure: what ran, where, how it ended, and what it printed.\n *\n * Every runner in this file produces one. A boolean is never enough: \"it\n * failed\" with no command, no exit code and no stderr is exactly how a broken\n * scaffold gets announced as a success.\n */\nexport type CommandResult = {\n /** Whether the command exited cleanly (code 0, no spawn error). */\n ok: boolean;\n /** The command as typed, e.g. `npm install`. */\n command: string;\n /** Directory the command ran in, when known. */\n cwd?: string;\n /** Exit code, or `null` when the process was signalled / never spawned. */\n code: number | null;\n /** Terminating signal, when the process was killed. */\n signal: NodeJS.Signals | null;\n stdout: string;\n stderr: string;\n /** Set when the process could not be spawned at all (ENOENT, EACCES, ...). */\n error?: Error;\n};\n\n/** Options accepted by the capturing runners. */\nexport type RunOptions = {\n /** Extra environment for the child; merged over `process.env`. */\n env?: NodeJS.ProcessEnv;\n};\n\n/**\n * The last command executed through {@link executeCommand}.\n *\n * `executeCommand` resolves a bare boolean (a contract several callers depend\n * on), so the *reason* a command failed would otherwise be lost. It is parked\n * here and claimed with {@link takeLastCommandOutput}, which clears it — a\n * caller can never accidentally attribute a stale failure to the wrong step.\n *\n * The scaffolder runs one command at a time, so a single slot is enough.\n */\nlet lastCommandOutput: CommandResult | undefined;\n\n/**\n * Claim (and clear) the output of the most recent {@link executeCommand} call.\n */\nexport function takeLastCommandOutput(): CommandResult | undefined {\n const output = lastCommandOutput;\n\n lastCommandOutput = undefined;\n\n return output;\n}\n\n/**\n * Keep only the last `lines` non-empty lines of a stream — enough context to\n * act on, without dumping a 500-line npm log over the wizard.\n */\nexport function tail(text: string, lines = 12): string {\n return text\n .split(/\\r?\\n/)\n .filter(line => line.trim().length > 0)\n .slice(-lines)\n .join(\"\\n\");\n}\n\nexport default async function exec(command: string, options: any = {}) {\n const [commandName, ...optionsList] = command.split(\" \");\n\n const commandOutput = childProcess.sync(commandName, optionsList, options);\n\n // it means command didn't end as expected, then stop the rest of the program\n if (commandOutput.error !== null) {\n process.exit(1);\n }\n\n return commandOutput;\n}\n\n/**\n * Attach stdout/stderr collectors to a child and return the accumulated text.\n *\n * The streams are optional on purpose: a child spawned with `stdio: \"ignore\"`\n * (or a test double) exposes none.\n */\nfunction collectOutput(child: ChildProcess) {\n const chunks = { stdout: \"\", stderr: \"\" };\n\n child.stdout?.on(\"data\", data => {\n chunks.stdout += String(data);\n });\n\n child.stderr?.on(\"data\", data => {\n chunks.stderr += String(data);\n });\n\n return chunks;\n}\n\nfunction logSpawnError(error: unknown) {\n if (!error) return;\n\n const message = (error as Error)?.message;\n\n log.error(colors.red(String(message ?? error)) + `\\n\\n`);\n}\n\n/**\n * This function directly executes a command\n *\n * Resolves a boolean for backwards compatibility; the full outcome (exit code\n * and captured output) is available to the caller via\n * {@link takeLastCommandOutput}.\n */\nexport async function executeCommand(cmd: string, args: string[], cwd: string) {\n const result = await runCapturedCommand(cmd, args, cwd);\n\n lastCommandOutput = result;\n\n return result.ok;\n}\n\n/**\n * Run a command to completion and resolve its full {@link CommandResult}.\n *\n * Output is piped and captured rather than discarded — the whole point is that\n * a failure can be explained instead of merely announced.\n */\nexport function runCapturedCommand(\n cmd: string,\n args: string[],\n cwd: string,\n options: RunOptions = {},\n): Promise<CommandResult> {\n return new Promise<CommandResult>(resolve => {\n const command = [cmd, ...args].join(\" \");\n\n const settle = (result: Partial<CommandResult>): void =>\n resolve({\n ok: false,\n command,\n cwd,\n code: null,\n signal: null,\n stdout: \"\",\n stderr: \"\",\n ...result,\n });\n\n let child: ChildProcess;\n\n try {\n child = spawn(cmd, args, {\n cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n env: options.env ? { ...process.env, ...options.env } : process.env,\n });\n } catch (error) {\n logSpawnError(error);\n\n return settle({ error: error as Error, stderr: String(error) });\n }\n\n const output = collectOutput(child);\n\n child.on(\"error\", error => {\n logSpawnError(error);\n\n settle({\n error: error as Error,\n stdout: output.stdout,\n stderr: output.stderr || String((error as Error)?.message ?? error),\n });\n });\n\n child.on(\"close\", (code, signal) => {\n settle({\n ok: code === 0,\n code,\n signal,\n stdout: output.stdout,\n stderr: output.stderr,\n });\n });\n });\n}\n\n/**\n * Run a long command with an abort handle.\n *\n * `install` stays a `Promise<boolean>` for existing callers; `result` carries\n * the exit code and the captured output so a failing install can be reported\n * with the command, its status and its stderr instead of a shrug.\n */\nexport function runCommand(\n cmd: string,\n args: string[],\n cwd: string,\n options: RunOptions = {},\n) {\n let child: ChildProcess;\n\n const result = new Promise<CommandResult>(resolve => {\n const command = [cmd, ...args].join(\" \");\n\n const settle = (partial: Partial<CommandResult>): void =>\n resolve({\n ok: false,\n command,\n cwd,\n code: null,\n signal: null,\n stdout: \"\",\n stderr: \"\",\n ...partial,\n });\n\n try {\n child = spawn(cmd, args, {\n cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n env: options.env ? { ...process.env, ...options.env } : process.env,\n });\n\n const output = collectOutput(child);\n\n child.on(\"error\", error => {\n logSpawnError(error);\n\n settle({\n error: error as Error,\n stdout: output.stdout,\n stderr: output.stderr || String((error as Error)?.message ?? error),\n });\n });\n\n child.on(\"close\", (code, signal) => {\n settle({\n ok: code === 0,\n code,\n signal,\n stdout: output.stdout,\n stderr: output.stderr,\n });\n });\n } catch (error) {\n settle({ error: error as Error, stderr: String(error) });\n }\n });\n\n const abort = async () => {\n if (child) {\n child.kill(\"SIGINT\");\n }\n };\n\n return { abort, install: result.then(outcome => outcome.ok), result };\n}\n"],"mappings":";;;;;;;;;;;;;;;AA8CA,IAAI;;;;AAKJ,SAAgB,wBAAmD;CACjE,MAAM,SAAS;CAEf,oBAAoB;CAEpB,OAAO;AACT;;;;;AAMA,SAAgB,KAAK,MAAc,QAAQ,IAAY;CACrD,OAAO,KACJ,MAAM,OAAO,CAAC,CACd,QAAO,SAAQ,KAAK,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,CACtC,MAAM,CAAC,KAAK,CAAC,CACb,KAAK,IAAI;AACd;;;;;;;AAqBA,SAAS,cAAc,OAAqB;CAC1C,MAAM,SAAS;EAAE,QAAQ;EAAI,QAAQ;CAAG;CAExC,MAAM,QAAQ,GAAG,SAAQ,SAAQ;EAC/B,OAAO,UAAU,OAAO,IAAI;CAC9B,CAAC;CAED,MAAM,QAAQ,GAAG,SAAQ,SAAQ;EAC/B,OAAO,UAAU,OAAO,IAAI;CAC9B,CAAC;CAED,OAAO;AACT;AAEA,SAAS,cAAc,OAAgB;CACrC,IAAI,CAAC,OAAO;CAEZ,MAAM,UAAW,OAAiB;CAElC,IAAI,MAAM,OAAO,IAAI,OAAO,WAAW,KAAK,CAAC,IAAI,MAAM;AACzD;;;;;;;;AASA,eAAsB,eAAe,KAAa,MAAgB,KAAa;CAC7E,MAAM,SAAS,MAAM,mBAAmB,KAAK,MAAM,GAAG;CAEtD,oBAAoB;CAEpB,OAAO,OAAO;AAChB;;;;;;;AAQA,SAAgB,mBACd,KACA,MACA,KACA,UAAsB,CAAC,GACC;CACxB,OAAO,IAAI,SAAuB,YAAW;EAC3C,MAAM,UAAU,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG;EAEvC,MAAM,UAAU,WACd,QAAQ;GACN,IAAI;GACJ;GACA;GACA,MAAM;GACN,QAAQ;GACR,QAAQ;GACR,QAAQ;GACR,GAAG;EACL,CAAC;EAEH,IAAI;EAEJ,IAAI;GACF,QAAQA,aAAM,KAAK,MAAM;IACvB;IACA,OAAO;KAAC;KAAU;KAAQ;IAAM;IAChC,KAAK,QAAQ,MAAM;KAAE,GAAG,QAAQ;KAAK,GAAG,QAAQ;IAAI,IAAI,QAAQ;GAClE,CAAC;EACH,SAAS,OAAO;GACd,cAAc,KAAK;GAEnB,OAAO,OAAO;IAAS;IAAgB,QAAQ,OAAO,KAAK;GAAE,CAAC;EAChE;EAEA,MAAM,SAAS,cAAc,KAAK;EAElC,MAAM,GAAG,UAAS,UAAS;GACzB,cAAc,KAAK;GAEnB,OAAO;IACE;IACP,QAAQ,OAAO;IACf,QAAQ,OAAO,UAAU,OAAQ,OAAiB,WAAW,KAAK;GACpE,CAAC;EACH,CAAC;EAED,MAAM,GAAG,UAAU,MAAM,WAAW;GAClC,OAAO;IACL,IAAI,SAAS;IACb;IACA;IACA,QAAQ,OAAO;IACf,QAAQ,OAAO;GACjB,CAAC;EACH,CAAC;CACH,CAAC;AACH;;;;;;;;AASA,SAAgB,WACd,KACA,MACA,KACA,UAAsB,CAAC,GACvB;CACA,IAAI;CAEJ,MAAM,SAAS,IAAI,SAAuB,YAAW;EACnD,MAAM,UAAU,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG;EAEvC,MAAM,UAAU,YACd,QAAQ;GACN,IAAI;GACJ;GACA;GACA,MAAM;GACN,QAAQ;GACR,QAAQ;GACR,QAAQ;GACR,GAAG;EACL,CAAC;EAEH,IAAI;GACF,QAAQA,aAAM,KAAK,MAAM;IACvB;IACA,OAAO;KAAC;KAAU;KAAQ;IAAM;IAChC,KAAK,QAAQ,MAAM;KAAE,GAAG,QAAQ;KAAK,GAAG,QAAQ;IAAI,IAAI,QAAQ;GAClE,CAAC;GAED,MAAM,SAAS,cAAc,KAAK;GAElC,MAAM,GAAG,UAAS,UAAS;IACzB,cAAc,KAAK;IAEnB,OAAO;KACE;KACP,QAAQ,OAAO;KACf,QAAQ,OAAO,UAAU,OAAQ,OAAiB,WAAW,KAAK;IACpE,CAAC;GACH,CAAC;GAED,MAAM,GAAG,UAAU,MAAM,WAAW;IAClC,OAAO;KACL,IAAI,SAAS;KACb;KACA;KACA,QAAQ,OAAO;KACf,QAAQ,OAAO;IACjB,CAAC;GACH,CAAC;EACH,SAAS,OAAO;GACd,OAAO;IAAS;IAAgB,QAAQ,OAAO,KAAK;GAAE,CAAC;EACzD;CACF,CAAC;CAED,MAAM,QAAQ,YAAY;EACxB,IAAI,OACF,MAAM,KAAK,QAAQ;CAEvB;CAEA,OAAO;EAAE;EAAO,SAAS,OAAO,MAAK,YAAW,QAAQ,EAAE;EAAG;CAAO;AACtE"}
@@ -7,19 +7,29 @@ import { copyDirectory, getFile, getJsonFile, putFile, putJsonFile, renameFile }
7
7
  import path from "path";
8
8
 
9
9
  //#region ../create-warlock/src/helpers/project-builder-helpers.ts
10
+ /**
11
+ * Bootstrap the project's git repository.
12
+ *
13
+ * Returns `false` as soon as a step fails, and leaves that step's captured
14
+ * output where the caller can claim it with `takeLastCommandOutput()`. It used
15
+ * to `return true` unconditionally, which meant a machine without git printed
16
+ * "Grimoire initialized!" over a directory that was not a repository.
17
+ */
10
18
  async function initializeGitRepository(appPath) {
11
- await executeCommand(`git`, ["init"], appPath);
12
- await executeCommand(`git`, [
13
- "checkout",
14
- "-b",
15
- "main"
16
- ], appPath);
17
- await executeCommand(`git`, ["add", "."], appPath);
18
- await executeCommand(`git`, [
19
- "commit",
20
- "-m",
21
- "Initial commit"
22
- ], appPath);
19
+ for (const args of [
20
+ ["init"],
21
+ [
22
+ "checkout",
23
+ "-b",
24
+ "main"
25
+ ],
26
+ ["add", "."],
27
+ [
28
+ "commit",
29
+ "-m",
30
+ "Initial commit"
31
+ ]
32
+ ]) if (!await executeCommand(`git`, args, appPath)) return false;
23
33
  return true;
24
34
  }
25
35
 
@@ -1 +1 @@
1
- {"version":3,"file":"project-builder-helpers.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/project-builder-helpers.ts"],"sourcesContent":["import { outro } from \"@clack/prompts\";\r\nimport { colors } from \"@mongez/copper\";\r\nimport {\r\n copyDirectory,\r\n getFile,\r\n getJsonFile,\r\n putFile,\r\n putJsonFile,\r\n renameFile,\r\n} from \"@warlock.js/fs\";\r\nimport path from \"path\";\r\nimport { executeCommand } from \"./exec\";\r\nimport { startCommand } from \"./package-manager\";\r\nimport { Template, template } from \"./paths\";\r\n\r\nexport async function initializeGitRepository(appPath: string) {\r\n // initialize git repository\r\n await executeCommand(`git`, [\"init\"], appPath);\r\n // switching to`main`branch\r\n await executeCommand(`git`, [\"checkout\", \"-b\", \"main\"], appPath);\r\n\r\n // add files\r\n await executeCommand(`git`, [\"add\", \".\"], appPath);\r\n\r\n // commit files\r\n await executeCommand(`git`, [\"commit\", \"-m\", \"Initial commit\"], appPath);\r\n\r\n return true;\r\n}\r\n\r\nexport async function updateEnvFile(appPath: string, appName: string) {\r\n // update package.json file\r\n const packageJson: any = getJsonFile(path.resolve(appPath, \"package.json\"));\r\n\r\n packageJson.name = appName;\r\n\r\n putJsonFile(path.resolve(appPath, \"package.json\"), packageJson);\r\n\r\n // update env file\r\n const dotEnv = getFile(path.resolve(appPath, \".env\"))\r\n .replace(\"AppName\", appName)\r\n .replace(\r\n \"AppCodeName\",\r\n appName\r\n .split(/-|_/g)\r\n .map(word => word[0])\r\n .join(\"\"),\r\n );\r\n\r\n putFile(path.resolve(appPath, \".env\"), dotEnv);\r\n\r\n // update .env.production file\r\n let dotEnvProduction = getFile(path.resolve(appPath, \".env.shared\"));\r\n\r\n dotEnvProduction = dotEnvProduction.replace(\"AppName\", appName).replace(\r\n \"AppCodeName\",\r\n appName\r\n .split(/-|_/g)\r\n .map(word => word[0])\r\n .join(\"\"),\r\n );\r\n\r\n putFile(path.resolve(appPath, \".env.shared\"), dotEnvProduction);\r\n}\r\n\r\nexport async function copyTemplateFiles(\r\n templateName: Template,\r\n appPath: string,\r\n _appName: string,\r\n) {\r\n // copy project files\r\n copyDirectory(template(templateName), appPath);\r\n\r\n // replace _.gitignore to\r\n renameFile(\r\n path.resolve(appPath, \"_.gitignore\"),\r\n path.resolve(appPath, \".gitignore\"),\r\n );\r\n}\r\n\r\nexport async function allDone(appName: string) {\r\n outro(\r\n \"Awesome! Your project is ready to rock! \" +\r\n \"Run the following command to start development:\",\r\n );\r\n\r\n console.log(colors.cyan(`cd ${appName} && ${startCommand()}`));\r\n\r\n console.log();\r\n\r\n console.log(\r\n `Pro tip: Install the ${colors.yellow(\r\n \"Generator Z\",\r\n )} extension in VSCode for helpful code snippets and productivity boosters!`,\r\n );\r\n}\r\n"],"mappings":";;;;;;;;;AAeA,eAAsB,wBAAwB,SAAiB;CAE7D,MAAM,eAAe,OAAO,CAAC,MAAM,GAAG,OAAO;CAE7C,MAAM,eAAe,OAAO;EAAC;EAAY;EAAM;CAAM,GAAG,OAAO;CAG/D,MAAM,eAAe,OAAO,CAAC,OAAO,GAAG,GAAG,OAAO;CAGjD,MAAM,eAAe,OAAO;EAAC;EAAU;EAAM;CAAgB,GAAG,OAAO;CAEvE,OAAO;AACT"}
1
+ {"version":3,"file":"project-builder-helpers.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/project-builder-helpers.ts"],"sourcesContent":["import { outro } from \"@clack/prompts\";\r\nimport { colors } from \"@mongez/copper\";\r\nimport {\r\n copyDirectory,\r\n getFile,\r\n getJsonFile,\r\n putFile,\r\n putJsonFile,\r\n renameFile,\r\n} from \"@warlock.js/fs\";\r\nimport path from \"path\";\r\nimport { executeCommand } from \"./exec\";\r\nimport { startCommand } from \"./package-manager\";\r\nimport { Template, template } from \"./paths\";\r\n\r\n/**\r\n * Bootstrap the project's git repository.\r\n *\r\n * Returns `false` as soon as a step fails, and leaves that step's captured\r\n * output where the caller can claim it with `takeLastCommandOutput()`. It used\r\n * to `return true` unconditionally, which meant a machine without git printed\r\n * \"Grimoire initialized!\" over a directory that was not a repository.\r\n */\r\nexport async function initializeGitRepository(appPath: string) {\r\n const steps: string[][] = [\r\n // initialize git repository\r\n [\"init\"],\r\n // switching to `main` branch\r\n [\"checkout\", \"-b\", \"main\"],\r\n // add files\r\n [\"add\", \".\"],\r\n // commit files\r\n [\"commit\", \"-m\", \"Initial commit\"],\r\n ];\r\n\r\n for (const args of steps) {\r\n const succeeded = await executeCommand(`git`, args, appPath);\r\n\r\n if (!succeeded) return false;\r\n }\r\n\r\n return true;\r\n}\r\n\r\nexport async function updateEnvFile(appPath: string, appName: string) {\r\n // update package.json file\r\n const packageJson: any = getJsonFile(path.resolve(appPath, \"package.json\"));\r\n\r\n packageJson.name = appName;\r\n\r\n putJsonFile(path.resolve(appPath, \"package.json\"), packageJson);\r\n\r\n // update env file\r\n const dotEnv = getFile(path.resolve(appPath, \".env\"))\r\n .replace(\"AppName\", appName)\r\n .replace(\r\n \"AppCodeName\",\r\n appName\r\n .split(/-|_/g)\r\n .map(word => word[0])\r\n .join(\"\"),\r\n );\r\n\r\n putFile(path.resolve(appPath, \".env\"), dotEnv);\r\n\r\n // update .env.production file\r\n let dotEnvProduction = getFile(path.resolve(appPath, \".env.shared\"));\r\n\r\n dotEnvProduction = dotEnvProduction.replace(\"AppName\", appName).replace(\r\n \"AppCodeName\",\r\n appName\r\n .split(/-|_/g)\r\n .map(word => word[0])\r\n .join(\"\"),\r\n );\r\n\r\n putFile(path.resolve(appPath, \".env.shared\"), dotEnvProduction);\r\n}\r\n\r\nexport async function copyTemplateFiles(\r\n templateName: Template,\r\n appPath: string,\r\n _appName: string,\r\n) {\r\n // copy project files\r\n copyDirectory(template(templateName), appPath);\r\n\r\n // replace _.gitignore to\r\n renameFile(\r\n path.resolve(appPath, \"_.gitignore\"),\r\n path.resolve(appPath, \".gitignore\"),\r\n );\r\n}\r\n\r\nexport async function allDone(appName: string) {\r\n outro(\r\n \"Awesome! Your project is ready to rock! \" +\r\n \"Run the following command to start development:\",\r\n );\r\n\r\n console.log(colors.cyan(`cd ${appName} && ${startCommand()}`));\r\n\r\n console.log();\r\n\r\n console.log(\r\n `Pro tip: Install the ${colors.yellow(\r\n \"Generator Z\",\r\n )} extension in VSCode for helpful code snippets and productivity boosters!`,\r\n );\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;AAuBA,eAAsB,wBAAwB,SAAiB;CAY7D,KAAK,MAAM,QAAQ;EATjB,CAAC,MAAM;EAEP;GAAC;GAAY;GAAM;EAAM;EAEzB,CAAC,OAAO,GAAG;EAEX;GAAC;GAAU;GAAM;EAAgB;CAGZ,GAGrB,IAAI,CAAC,MAFmB,eAAe,OAAO,MAAM,OAAO,GAE3C,OAAO;CAGzB,OAAO;AACT"}
@@ -0,0 +1,166 @@
1
+ import { packageRoot, template } from "./paths.mjs";
2
+ import { getJsonFile } from "@warlock.js/fs";
3
+
4
+ //#region ../create-warlock/src/helpers/warlock-versions.ts
5
+ /**
6
+ * Resolving the version to stamp onto the generated project's `@warlock.js/*`
7
+ * dependencies.
8
+ *
9
+ * ## Why this file exists
10
+ *
11
+ * The scaffolder used to stamp its OWN version onto every sibling package
12
+ * (`"@warlock.js/core": "4.16.2"`). That is only correct while the scaffolder's
13
+ * version is published — and it usually is not: the release tooling bumps the
14
+ * source version on every build, including `--no-publish` builds, so between
15
+ * publishes the working tree carries a version that exists nowhere on the
16
+ * registry. Every project scaffolded in that window pinned eight dependencies
17
+ * to a version npm cannot resolve, and the install died with `ETARGET`.
18
+ *
19
+ * ## The rule
20
+ *
21
+ * Never write a dependency version we have not established exists. Resolution
22
+ * order, per package:
23
+ *
24
+ * 1. the scaffolder's own version, IF the registry has it published — this
25
+ * preserves the lockstep guarantee the pin was introduced for;
26
+ * 2. otherwise the registry's `latest` — the newest thing that actually
27
+ * exists, with a note explaining the substitution;
28
+ * 3. otherwise (registry unreachable) a caret range floored to the major,
29
+ * e.g. `^4.0.0` — always satisfiable by any published 4.x, and the
30
+ * scaffold-time install writes a lockfile that freezes the result anyway.
31
+ *
32
+ * A package that resolves to nothing (404 — never published) is reported, not
33
+ * papered over: it is the difference between "your install failed" and "the
34
+ * feature you asked for does not exist yet".
35
+ */
36
+ const REGISTRY_TIMEOUT_MS = 6e3;
37
+ /**
38
+ * Registry to query. `npm_config_registry` is set by npm/npx when the
39
+ * scaffolder runs through them, so a private mirror is honoured for free.
40
+ */
41
+ function registryUrl() {
42
+ return (process.env.npm_config_registry?.trim() || "https://registry.npmjs.org").replace(/\/+$/, "");
43
+ }
44
+ /** `4.16.2` -> `^4.0.0`; anything unparseable -> `latest`. */
45
+ function fallbackRange(version) {
46
+ const major = /^\s*v?(\d+)\./.exec(version)?.[1];
47
+ return major ? `^${major}.0.0` : "latest";
48
+ }
49
+ /**
50
+ * Fetch the abbreviated packument for a package. Returns `undefined` when the
51
+ * registry cannot be reached (network / timeout) and `null` when the registry
52
+ * answers that the package does not exist.
53
+ */
54
+ async function fetchPackument(packageName) {
55
+ const url = `${registryUrl()}/${packageName.replace("/", "%2F")}`;
56
+ try {
57
+ const response = await fetch(url, {
58
+ headers: { accept: "application/vnd.npm.install-v1+json" },
59
+ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS)
60
+ });
61
+ if (response.status === 404 || response.status === 401) return null;
62
+ if (!response.ok) return void 0;
63
+ return await response.json();
64
+ } catch {
65
+ return;
66
+ }
67
+ }
68
+ /**
69
+ * Resolve one package against the registry. Pure aside from the fetch, so the
70
+ * decision table above is readable in one place.
71
+ */
72
+ async function resolvePackage(packageName, ownVersion) {
73
+ const packument = await fetchPackument(packageName);
74
+ if (packument === void 0) return {
75
+ package: packageName,
76
+ version: fallbackRange(ownVersion),
77
+ source: "range-fallback",
78
+ published: true,
79
+ reachable: false
80
+ };
81
+ if (packument === null) return {
82
+ package: packageName,
83
+ version: fallbackRange(ownVersion),
84
+ source: "range-fallback",
85
+ published: false,
86
+ reachable: true
87
+ };
88
+ if (packument.versions?.[ownVersion]) return {
89
+ package: packageName,
90
+ version: ownVersion,
91
+ source: "own-version",
92
+ published: true,
93
+ reachable: true
94
+ };
95
+ const latest = packument["dist-tags"]?.latest;
96
+ if (latest) return {
97
+ package: packageName,
98
+ version: latest,
99
+ source: "registry-latest",
100
+ published: true,
101
+ reachable: true
102
+ };
103
+ return {
104
+ package: packageName,
105
+ version: fallbackRange(ownVersion),
106
+ source: "range-fallback",
107
+ published: true,
108
+ reachable: true
109
+ };
110
+ }
111
+ /**
112
+ * Every `@warlock.js/*` dependency the template declares. Read from the
113
+ * template rather than the copied project so resolution can start before (or
114
+ * in parallel with) the copy.
115
+ */
116
+ function templateWarlockDependencies() {
117
+ const templatePackageJson = getJsonFile(`${template("warlock")}/package.json`);
118
+ const names = /* @__PURE__ */ new Set();
119
+ for (const field of ["dependencies", "devDependencies"]) for (const name of Object.keys(templatePackageJson[field] ?? {})) if (name.startsWith("@warlock.js/")) names.add(name);
120
+ return [...names];
121
+ }
122
+ /** The scaffolder's own published version, i.e. the lockstep candidate. */
123
+ function scaffolderVersion() {
124
+ return getJsonFile(packageRoot("package.json")).version;
125
+ }
126
+ let cached;
127
+ /**
128
+ * Resolve the versions to stamp, once per process.
129
+ *
130
+ * Never throws and never blocks a scaffold: the worst case is the caret
131
+ * fallback plus a note saying so.
132
+ */
133
+ function resolveWarlockVersions(packages = templateWarlockDependencies(), ownVersion = scaffolderVersion()) {
134
+ if (cached) return cached;
135
+ cached = (async () => {
136
+ const resolutions = await Promise.all(packages.map((name) => resolvePackage(name, ownVersion)));
137
+ const versions = {};
138
+ const notes = [];
139
+ const unpublished = [];
140
+ const substituted = [];
141
+ let offline = false;
142
+ for (const resolution of resolutions) {
143
+ versions[resolution.package] = resolution.version;
144
+ if (!resolution.reachable) offline = true;
145
+ if (!resolution.published) unpublished.push(resolution.package);
146
+ if (resolution.source === "registry-latest") substituted.push(resolution);
147
+ }
148
+ if (offline) notes.push(`Could not reach the npm registry — pinning @warlock.js/* to ${fallbackRange(ownVersion)} instead of an exact version.`);
149
+ if (substituted.length > 0) {
150
+ const latest = substituted[0].version;
151
+ notes.push(`create-warlock ${ownVersion} is not published yet — pinning @warlock.js/* to the latest published version (${latest}).`);
152
+ }
153
+ if (unpublished.length > 0) notes.push(`Not published on the registry: ${unpublished.join(", ")} — the install will fail until they are released.`);
154
+ return {
155
+ versions,
156
+ notes,
157
+ unpublished,
158
+ offline
159
+ };
160
+ })();
161
+ return cached;
162
+ }
163
+
164
+ //#endregion
165
+ export { fallbackRange, resolveWarlockVersions };
166
+ //# sourceMappingURL=warlock-versions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"warlock-versions.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/warlock-versions.ts"],"sourcesContent":["/**\n * Resolving the version to stamp onto the generated project's `@warlock.js/*`\n * dependencies.\n *\n * ## Why this file exists\n *\n * The scaffolder used to stamp its OWN version onto every sibling package\n * (`\"@warlock.js/core\": \"4.16.2\"`). That is only correct while the scaffolder's\n * version is published — and it usually is not: the release tooling bumps the\n * source version on every build, including `--no-publish` builds, so between\n * publishes the working tree carries a version that exists nowhere on the\n * registry. Every project scaffolded in that window pinned eight dependencies\n * to a version npm cannot resolve, and the install died with `ETARGET`.\n *\n * ## The rule\n *\n * Never write a dependency version we have not established exists. Resolution\n * order, per package:\n *\n * 1. the scaffolder's own version, IF the registry has it published — this\n * preserves the lockstep guarantee the pin was introduced for;\n * 2. otherwise the registry's `latest` — the newest thing that actually\n * exists, with a note explaining the substitution;\n * 3. otherwise (registry unreachable) a caret range floored to the major,\n * e.g. `^4.0.0` — always satisfiable by any published 4.x, and the\n * scaffold-time install writes a lockfile that freezes the result anyway.\n *\n * A package that resolves to nothing (404 — never published) is reported, not\n * papered over: it is the difference between \"your install failed\" and \"the\n * feature you asked for does not exist yet\".\n */\n\nimport { getJsonFile } from \"@warlock.js/fs\";\nimport { packageRoot, template } from \"./paths\";\n\nexport type VersionSource =\n \"own-version\" | \"registry-latest\" | \"range-fallback\";\n\nexport type ResolvedVersion = {\n package: string;\n /** The exact version or range to write into the generated package.json. */\n version: string;\n source: VersionSource;\n};\n\nexport type VersionResolution = {\n /** package name -> version/range to stamp. */\n versions: Record<string, string>;\n /** Human-readable notes worth showing before the install runs. */\n notes: string[];\n /** Packages the registry does not know about at all. */\n unpublished: string[];\n /** True when the registry could not be reached and ranges were guessed. */\n offline: boolean;\n};\n\nconst REGISTRY_TIMEOUT_MS = 6_000;\n\n/**\n * Registry to query. `npm_config_registry` is set by npm/npx when the\n * scaffolder runs through them, so a private mirror is honoured for free.\n */\nfunction registryUrl(): string {\n const registry =\n process.env.npm_config_registry?.trim() || \"https://registry.npmjs.org\";\n\n return registry.replace(/\\/+$/, \"\");\n}\n\n/** `4.16.2` -> `^4.0.0`; anything unparseable -> `latest`. */\nexport function fallbackRange(version: string): string {\n const major = /^\\s*v?(\\d+)\\./.exec(version)?.[1];\n\n return major ? `^${major}.0.0` : \"latest\";\n}\n\ntype Packument = {\n \"dist-tags\"?: Record<string, string>;\n versions?: Record<string, unknown>;\n};\n\n/**\n * Fetch the abbreviated packument for a package. Returns `undefined` when the\n * registry cannot be reached (network / timeout) and `null` when the registry\n * answers that the package does not exist.\n */\nasync function fetchPackument(\n packageName: string,\n): Promise<Packument | null | undefined> {\n const url = `${registryUrl()}/${packageName.replace(\"/\", \"%2F\")}`;\n\n try {\n const response = await fetch(url, {\n headers: { accept: \"application/vnd.npm.install-v1+json\" },\n signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),\n });\n\n // 404 (public) and 401 (scoped package the registry hides) both mean the\n // same thing to us: there is nothing here to install.\n if (response.status === 404 || response.status === 401) return null;\n\n if (!response.ok) return undefined;\n\n return (await response.json()) as Packument;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolve one package against the registry. Pure aside from the fetch, so the\n * decision table above is readable in one place.\n */\nasync function resolvePackage(\n packageName: string,\n ownVersion: string,\n): Promise<ResolvedVersion & { published: boolean; reachable: boolean }> {\n const packument = await fetchPackument(packageName);\n\n if (packument === undefined) {\n return {\n package: packageName,\n version: fallbackRange(ownVersion),\n source: \"range-fallback\",\n published: true,\n reachable: false,\n };\n }\n\n if (packument === null) {\n return {\n package: packageName,\n version: fallbackRange(ownVersion),\n source: \"range-fallback\",\n published: false,\n reachable: true,\n };\n }\n\n if (packument.versions?.[ownVersion]) {\n return {\n package: packageName,\n version: ownVersion,\n source: \"own-version\",\n published: true,\n reachable: true,\n };\n }\n\n const latest = packument[\"dist-tags\"]?.latest;\n\n if (latest) {\n return {\n package: packageName,\n version: latest,\n source: \"registry-latest\",\n published: true,\n reachable: true,\n };\n }\n\n return {\n package: packageName,\n version: fallbackRange(ownVersion),\n source: \"range-fallback\",\n published: true,\n reachable: true,\n };\n}\n\n/**\n * Every `@warlock.js/*` dependency the template declares. Read from the\n * template rather than the copied project so resolution can start before (or\n * in parallel with) the copy.\n */\nexport function templateWarlockDependencies(): string[] {\n const templatePackageJson = getJsonFile(\n `${template(\"warlock\")}/package.json`,\n ) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n\n const names = new Set<string>();\n\n for (const field of [\"dependencies\", \"devDependencies\"] as const) {\n for (const name of Object.keys(templatePackageJson[field] ?? {})) {\n if (name.startsWith(\"@warlock.js/\")) names.add(name);\n }\n }\n\n return [...names];\n}\n\n/** The scaffolder's own published version, i.e. the lockstep candidate. */\nexport function scaffolderVersion(): string {\n return (getJsonFile(packageRoot(\"package.json\")) as { version: string })\n .version;\n}\n\nlet cached: Promise<VersionResolution> | undefined;\n\n/**\n * Resolve the versions to stamp, once per process.\n *\n * Never throws and never blocks a scaffold: the worst case is the caret\n * fallback plus a note saying so.\n */\nexport function resolveWarlockVersions(\n packages: string[] = templateWarlockDependencies(),\n ownVersion: string = scaffolderVersion(),\n): Promise<VersionResolution> {\n if (cached) return cached;\n\n cached = (async (): Promise<VersionResolution> => {\n const resolutions = await Promise.all(\n packages.map(name => resolvePackage(name, ownVersion)),\n );\n\n const versions: Record<string, string> = {};\n const notes: string[] = [];\n const unpublished: string[] = [];\n const substituted: ResolvedVersion[] = [];\n\n let offline = false;\n\n for (const resolution of resolutions) {\n versions[resolution.package] = resolution.version;\n\n if (!resolution.reachable) offline = true;\n if (!resolution.published) unpublished.push(resolution.package);\n if (resolution.source === \"registry-latest\") substituted.push(resolution);\n }\n\n if (offline) {\n notes.push(\n `Could not reach the npm registry — pinning @warlock.js/* to ${fallbackRange(ownVersion)} instead of an exact version.`,\n );\n }\n\n if (substituted.length > 0) {\n const latest = substituted[0].version;\n\n notes.push(\n `create-warlock ${ownVersion} is not published yet — pinning @warlock.js/* to the latest published version (${latest}).`,\n );\n }\n\n if (unpublished.length > 0) {\n notes.push(\n `Not published on the registry: ${unpublished.join(\", \")} — the install will fail until they are released.`,\n );\n }\n\n return { versions, notes, unpublished, offline };\n })();\n\n return cached;\n}\n\n/** Test/CLI seam: forget the memoized resolution. */\nexport function resetWarlockVersionsCache() {\n cached = undefined;\n}\n"],"mappings":"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"}
package/esm/index.mjs CHANGED
@@ -79,7 +79,13 @@ function splitList(value) {
79
79
  return value.split(",").map((item) => item.trim()).filter(Boolean);
80
80
  }
81
81
  function createApp() {
82
- createNewApp(parseFlags(process.argv.slice(2)));
82
+ const flags = parseFlags(process.argv.slice(2));
83
+ Promise.resolve(createNewApp(flags)).catch((error) => {
84
+ console.error();
85
+ console.error(` create-warlock failed: ${error?.message ?? String(error)}`);
86
+ console.error();
87
+ process.exit(1);
88
+ });
83
89
  }
84
90
 
85
91
  //#endregion
package/esm/index.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../create-warlock/src/index.ts"],"sourcesContent":["import createNewApp from \"./commands/create-new-app\";\nimport { CliFlags } from \"./commands/create-new-app/types\";\nimport { NO_DATABASE } from \"./features/database-drivers\";\n\nconst valueFlags = [\"name\", \"db\", \"pm\", \"features\", \"ai\"];\n\n/**\n * Parse the scaffolder's own CLI flags for non-interactive mode.\n *\n * @example\n * create-warlock my-app --db=postgres --features=test,herald --ai=openai,anthropic --yes\n */\nexport function parseFlags(argv: string[]): CliFlags {\n const flags: CliFlags = {};\n const positionals: string[] = [];\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n\n if (!arg.startsWith(\"-\")) {\n positionals.push(arg);\n continue;\n }\n\n const equalIndex = arg.indexOf(\"=\");\n const key = (equalIndex === -1 ? arg : arg.slice(0, equalIndex)).replace(/^-+/, \"\");\n let value: string | undefined = equalIndex === -1 ? undefined : arg.slice(equalIndex + 1);\n\n // Value-taking flags may use either `--key=value` or `--key value`.\n if (valueFlags.includes(key) && value === undefined) {\n const next = argv[i + 1];\n\n if (next && !next.startsWith(\"-\")) {\n value = next;\n i++;\n }\n }\n\n switch (key) {\n case \"yes\":\n case \"y\":\n flags.yes = true;\n break;\n case \"git\":\n flags.git = true;\n break;\n case \"no-git\":\n flags.git = false;\n break;\n case \"jwt\":\n flags.jwt = true;\n break;\n case \"no-jwt\":\n flags.jwt = false;\n break;\n case \"name\":\n flags.name = value;\n break;\n case \"db\":\n flags.db = value;\n break;\n case \"no-db\":\n // Opt out of a database entirely — equivalent to `--db=none`.\n flags.db = NO_DATABASE;\n break;\n case \"pm\":\n flags.pm = value;\n break;\n case \"features\":\n flags.features = splitList(value);\n break;\n case \"ai\":\n flags.ai = splitList(value);\n break;\n }\n }\n\n if (!flags.name && positionals.length > 0) {\n flags.name = positionals[0];\n }\n\n return flags;\n}\n\nfunction splitList(value: string | undefined): string[] {\n if (!value) return [];\n\n return value\n .split(\",\")\n .map(item => item.trim())\n .filter(Boolean);\n}\n\nexport default function createApp() {\n const flags = parseFlags(process.argv.slice(2));\n\n createNewApp(flags);\n}\n"],"mappings":";;;;AAIA,MAAM,aAAa;CAAC;CAAQ;CAAM;CAAM;CAAY;AAAI;;;;;;;AAQxD,SAAgB,WAAW,MAA0B;CACnD,MAAM,QAAkB,CAAC;CACzB,MAAM,cAAwB,CAAC;CAE/B,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EAEjB,IAAI,CAAC,IAAI,WAAW,GAAG,GAAG;GACxB,YAAY,KAAK,GAAG;GACpB;EACF;EAEA,MAAM,aAAa,IAAI,QAAQ,GAAG;EAClC,MAAM,OAAO,eAAe,KAAK,MAAM,IAAI,MAAM,GAAG,UAAU,EAAC,CAAE,QAAQ,OAAO,EAAE;EAClF,IAAI,QAA4B,eAAe,KAAK,SAAY,IAAI,MAAM,aAAa,CAAC;EAGxF,IAAI,WAAW,SAAS,GAAG,KAAK,UAAU,QAAW;GACnD,MAAM,OAAO,KAAK,IAAI;GAEtB,IAAI,QAAQ,CAAC,KAAK,WAAW,GAAG,GAAG;IACjC,QAAQ;IACR;GACF;EACF;EAEA,QAAQ,KAAR;GACE,KAAK;GACL,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,OAAO;IACb;GACF,KAAK;IACH,MAAM,KAAK;IACX;GACF,KAAK;IAEH,MAAM,KAAK;IACX;GACF,KAAK;IACH,MAAM,KAAK;IACX;GACF,KAAK;IACH,MAAM,WAAW,UAAU,KAAK;IAChC;GACF,KAAK;IACH,MAAM,KAAK,UAAU,KAAK;IAC1B;EACJ;CACF;CAEA,IAAI,CAAC,MAAM,QAAQ,YAAY,SAAS,GACtC,MAAM,OAAO,YAAY;CAG3B,OAAO;AACT;AAEA,SAAS,UAAU,OAAqC;CACtD,IAAI,CAAC,OAAO,OAAO,CAAC;CAEpB,OAAO,MACJ,MAAM,GAAG,CAAC,CACV,KAAI,SAAQ,KAAK,KAAK,CAAC,CAAC,CACxB,OAAO,OAAO;AACnB;AAEA,SAAwB,YAAY;CAGlC,aAFc,WAAW,QAAQ,KAAK,MAAM,CAAC,CAE5B,CAAC;AACpB"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../create-warlock/src/index.ts"],"sourcesContent":["import createNewApp from \"./commands/create-new-app\";\nimport { CliFlags } from \"./commands/create-new-app/types\";\nimport { NO_DATABASE } from \"./features/database-drivers\";\n\nconst valueFlags = [\"name\", \"db\", \"pm\", \"features\", \"ai\"];\n\n/**\n * Parse the scaffolder's own CLI flags for non-interactive mode.\n *\n * @example\n * create-warlock my-app --db=postgres --features=test,herald --ai=openai,anthropic --yes\n */\nexport function parseFlags(argv: string[]): CliFlags {\n const flags: CliFlags = {};\n const positionals: string[] = [];\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n\n if (!arg.startsWith(\"-\")) {\n positionals.push(arg);\n continue;\n }\n\n const equalIndex = arg.indexOf(\"=\");\n const key = (equalIndex === -1 ? arg : arg.slice(0, equalIndex)).replace(/^-+/, \"\");\n let value: string | undefined = equalIndex === -1 ? undefined : arg.slice(equalIndex + 1);\n\n // Value-taking flags may use either `--key=value` or `--key value`.\n if (valueFlags.includes(key) && value === undefined) {\n const next = argv[i + 1];\n\n if (next && !next.startsWith(\"-\")) {\n value = next;\n i++;\n }\n }\n\n switch (key) {\n case \"yes\":\n case \"y\":\n flags.yes = true;\n break;\n case \"git\":\n flags.git = true;\n break;\n case \"no-git\":\n flags.git = false;\n break;\n case \"jwt\":\n flags.jwt = true;\n break;\n case \"no-jwt\":\n flags.jwt = false;\n break;\n case \"name\":\n flags.name = value;\n break;\n case \"db\":\n flags.db = value;\n break;\n case \"no-db\":\n // Opt out of a database entirely — equivalent to `--db=none`.\n flags.db = NO_DATABASE;\n break;\n case \"pm\":\n flags.pm = value;\n break;\n case \"features\":\n flags.features = splitList(value);\n break;\n case \"ai\":\n flags.ai = splitList(value);\n break;\n }\n }\n\n if (!flags.name && positionals.length > 0) {\n flags.name = positionals[0];\n }\n\n return flags;\n}\n\nfunction splitList(value: string | undefined): string[] {\n if (!value) return [];\n\n return value\n .split(\",\")\n .map(item => item.trim())\n .filter(Boolean);\n}\n\nexport default function createApp() {\n const flags = parseFlags(process.argv.slice(2));\n\n // An unexpected throw must surface as a readable error AND a non-zero exit\n // code — never as a stack trace the user scrolls past on the way to a green\n // banner (there is no banner after this point).\n Promise.resolve(createNewApp(flags)).catch((error: unknown) => {\n console.error();\n console.error(\n ` create-warlock failed: ${(error as Error)?.message ?? String(error)}`,\n );\n console.error();\n\n process.exit(1);\n });\n}\n"],"mappings":";;;;AAIA,MAAM,aAAa;CAAC;CAAQ;CAAM;CAAM;CAAY;AAAI;;;;;;;AAQxD,SAAgB,WAAW,MAA0B;CACnD,MAAM,QAAkB,CAAC;CACzB,MAAM,cAAwB,CAAC;CAE/B,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EAEjB,IAAI,CAAC,IAAI,WAAW,GAAG,GAAG;GACxB,YAAY,KAAK,GAAG;GACpB;EACF;EAEA,MAAM,aAAa,IAAI,QAAQ,GAAG;EAClC,MAAM,OAAO,eAAe,KAAK,MAAM,IAAI,MAAM,GAAG,UAAU,EAAC,CAAE,QAAQ,OAAO,EAAE;EAClF,IAAI,QAA4B,eAAe,KAAK,SAAY,IAAI,MAAM,aAAa,CAAC;EAGxF,IAAI,WAAW,SAAS,GAAG,KAAK,UAAU,QAAW;GACnD,MAAM,OAAO,KAAK,IAAI;GAEtB,IAAI,QAAQ,CAAC,KAAK,WAAW,GAAG,GAAG;IACjC,QAAQ;IACR;GACF;EACF;EAEA,QAAQ,KAAR;GACE,KAAK;GACL,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,OAAO;IACb;GACF,KAAK;IACH,MAAM,KAAK;IACX;GACF,KAAK;IAEH,MAAM,KAAK;IACX;GACF,KAAK;IACH,MAAM,KAAK;IACX;GACF,KAAK;IACH,MAAM,WAAW,UAAU,KAAK;IAChC;GACF,KAAK;IACH,MAAM,KAAK,UAAU,KAAK;IAC1B;EACJ;CACF;CAEA,IAAI,CAAC,MAAM,QAAQ,YAAY,SAAS,GACtC,MAAM,OAAO,YAAY;CAG3B,OAAO;AACT;AAEA,SAAS,UAAU,OAAqC;CACtD,IAAI,CAAC,OAAO,OAAO,CAAC;CAEpB,OAAO,MACJ,MAAM,GAAG,CAAC,CACV,KAAI,SAAQ,KAAK,KAAK,CAAC,CAAC,CACxB,OAAO,OAAO;AACnB;AAEA,SAAwB,YAAY;CAClC,MAAM,QAAQ,WAAW,QAAQ,KAAK,MAAM,CAAC,CAAC;CAK9C,QAAQ,QAAQ,aAAa,KAAK,CAAC,CAAC,CAAC,OAAO,UAAmB;EAC7D,QAAQ,MAAM;EACd,QAAQ,MACN,4BAA6B,OAAiB,WAAW,OAAO,KAAK,GACvE;EACA,QAAQ,MAAM;EAEd,QAAQ,KAAK,CAAC;CAChB,CAAC;AACH"}
@@ -0,0 +1,98 @@
1
+ import { tail } from "../helpers/exec.mjs";
2
+ import { colors } from "@mongez/copper";
3
+
4
+ //#region ../create-warlock/src/ui/report.ts
5
+ const bullet = colors.red("✖");
6
+ /**
7
+ * The command line, its exit status, and the tail of what it printed.
8
+ */
9
+ function describeCommand(result) {
10
+ const lines = [];
11
+ lines.push(`${colors.dim("command:")} ${colors.white(result.command)}`);
12
+ if (result.cwd) lines.push(`${colors.dim("in:")} ${colors.white(result.cwd)}`);
13
+ if (result.error) lines.push(`${colors.dim("failed:")} ${colors.white(result.error.message || String(result.error))}`);
14
+ else if (result.signal) lines.push(`${colors.dim("killed:")} ${colors.white(result.signal)}`);
15
+ else lines.push(`${colors.dim("exited:")} ${colors.white(`code ${result.code}`)}`);
16
+ const output = tail(result.stderr) || tail(result.stdout);
17
+ if (output) {
18
+ lines.push(colors.dim("output:"));
19
+ for (const line of output.split("\n")) lines.push(` ${colors.dim(line)}`);
20
+ }
21
+ return lines;
22
+ }
23
+ function printProblem(problem) {
24
+ console.log(` ${bullet} ${colors.bold(colors.red(problem.step))}`);
25
+ console.log(` ${colors.white(problem.detail)}`);
26
+ if (problem.result) for (const line of describeCommand(problem.result)) console.log(` ${line}`);
27
+ for (const hint of problem.hints ?? []) console.log(` ${colors.yellow("→")} ${colors.yellow(hint)}`);
28
+ console.log();
29
+ }
30
+ /**
31
+ * Report a failure the scaffold cannot continue past, and leave the process
32
+ * with a non-zero exit code. Nothing after this point may print a success.
33
+ */
34
+ function failFatally(problem) {
35
+ console.log();
36
+ console.log(colors.bold(colors.red(" SCAFFOLD FAILED")));
37
+ console.log();
38
+ printProblem(problem);
39
+ console.log(colors.dim(" The project directory was left in place so you can inspect it."));
40
+ console.log();
41
+ process.exit(1);
42
+ }
43
+ /**
44
+ * Report the steps that failed on a scaffold that otherwise completed, and say
45
+ * plainly what the project does NOT have as a result.
46
+ */
47
+ function showProblems(problems) {
48
+ if (problems.length === 0) return;
49
+ console.log();
50
+ console.log(colors.bold(colors.yellow(` COMPLETED WITH ${problems.length} PROBLEM${problems.length === 1 ? "" : "S"}`)));
51
+ console.log();
52
+ for (const problem of problems) printProblem(problem);
53
+ }
54
+ /** Neutral, non-failing information — e.g. which versions got pinned and why. */
55
+ function showNotes(notes) {
56
+ for (const note of notes) console.log(` ${colors.yellow("!")} ${colors.dim(note)}`);
57
+ if (notes.length > 0) console.log();
58
+ }
59
+ /**
60
+ * A scaffold that finished with problems still produced a project, so print the
61
+ * same facts the success screen would — minus the celebration, and listing only
62
+ * what is actually installed.
63
+ */
64
+ function showPartialScreen(options) {
65
+ const { projectName, database, features, missingFeatures, packageManager } = options;
66
+ const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
67
+ console.log(` ${colors.bold(colors.yellow("⚠ PROJECT CREATED — BUT NOT AS REQUESTED"))}`);
68
+ console.log();
69
+ console.log(` ${colors.dim("Project: ")}${colors.white(projectName)}`);
70
+ console.log(` ${colors.dim("Database: ")}${colors.white(database)}`);
71
+ console.log(` ${colors.dim("Installed:")}${colors.white(features.length > 0 ? " " + features.join(", ") : " none")}`);
72
+ if (missingFeatures.length > 0) console.log(` ${colors.dim("Missing: ")}${colors.red(missingFeatures.join(", "))}`);
73
+ console.log();
74
+ console.log(` ${colors.dim("Fix the problems above, then:")}`);
75
+ console.log();
76
+ console.log(` ${colors.cyan("cd")} ${projectName}`);
77
+ if (missingFeatures.length > 0) console.log(` ${colors.cyan(`npx warlock add ${missingFeatures.join(" ")}`)}`);
78
+ console.log(` ${colors.cyan(devCommand)}`);
79
+ console.log();
80
+ }
81
+ /**
82
+ * Turn an npm/yarn/pnpm failure into actionable guidance where we can
83
+ * recognise it. `ETARGET` in particular is the signature of a dependency
84
+ * pinned to a version that was never published — the bug this whole reporting
85
+ * path was written for.
86
+ */
87
+ function installFailureHints(result) {
88
+ const hints = ["Nothing was installed. Fix the error above, then run the install again inside the project."];
89
+ const output = `${result?.stdout ?? ""}${result?.stderr ?? ""}`;
90
+ if (/ETARGET|No matching version found/i.test(output)) hints.push("A dependency is pinned to a version that does not exist on the registry — check the @warlock.js/* versions in package.json.");
91
+ if (/ENOTFOUND|ETIMEDOUT|ECONNREFUSED|network/i.test(output)) hints.push("The registry was unreachable — check your network or proxy.");
92
+ if (/EACCES|EPERM/i.test(output)) hints.push("Permission denied — check the directory's ownership before retrying.");
93
+ return hints;
94
+ }
95
+
96
+ //#endregion
97
+ export { failFatally, installFailureHints, showNotes, showPartialScreen, showProblems };
98
+ //# sourceMappingURL=report.mjs.map