outfitter 0.2.2 → 0.2.3

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 (135) hide show
  1. package/README.md +215 -116
  2. package/dist/actions.d.ts +2 -0
  3. package/dist/actions.js +34 -0
  4. package/dist/cli.js +3 -1
  5. package/dist/commands/add.d.ts +54 -0
  6. package/dist/commands/add.js +16 -0
  7. package/dist/commands/check.d.ts +91 -0
  8. package/dist/commands/check.js +14 -0
  9. package/dist/commands/demo.d.ts +21 -0
  10. package/dist/commands/demo.js +8 -0
  11. package/dist/commands/docs-module-loader.d.ts +2 -0
  12. package/dist/commands/docs-module-loader.js +8 -0
  13. package/dist/commands/doctor.d.ts +2 -0
  14. package/dist/commands/doctor.js +13 -0
  15. package/dist/commands/init.d.ts +7 -0
  16. package/dist/commands/init.js +31 -0
  17. package/dist/commands/migrate-kit.d.ts +2 -0
  18. package/dist/commands/migrate-kit.js +15 -0
  19. package/dist/commands/repo.d.ts +3 -0
  20. package/dist/commands/repo.js +9 -0
  21. package/dist/commands/scaffold.d.ts +4 -0
  22. package/dist/commands/scaffold.js +31 -0
  23. package/dist/commands/shared-deps.d.ts +36 -0
  24. package/dist/commands/shared-deps.js +10 -0
  25. package/dist/commands/update-planner.d.ts +58 -0
  26. package/dist/commands/update-planner.js +8 -0
  27. package/dist/commands/update-workspace.d.ts +76 -0
  28. package/dist/commands/update-workspace.js +16 -0
  29. package/dist/commands/update.d.ts +113 -0
  30. package/dist/commands/update.js +21 -0
  31. package/dist/create/index.d.ts +5 -0
  32. package/dist/create/index.js +29 -0
  33. package/dist/create/planner.d.ts +3 -0
  34. package/dist/create/planner.js +21 -0
  35. package/dist/create/presets.d.ts +3 -0
  36. package/dist/create/presets.js +12 -0
  37. package/dist/create/types.d.ts +2 -0
  38. package/dist/create/types.js +1 -0
  39. package/dist/engine/blocks.d.ts +3 -0
  40. package/dist/engine/blocks.js +12 -0
  41. package/dist/engine/collector.d.ts +2 -0
  42. package/dist/engine/collector.js +8 -0
  43. package/dist/engine/config.d.ts +3 -0
  44. package/dist/engine/config.js +12 -0
  45. package/dist/engine/executor.d.ts +3 -0
  46. package/dist/engine/executor.js +16 -0
  47. package/dist/engine/index.d.ts +8 -0
  48. package/dist/engine/index.js +59 -0
  49. package/dist/engine/names.d.ts +2 -0
  50. package/dist/engine/names.js +16 -0
  51. package/dist/engine/post-scaffold.d.ts +3 -0
  52. package/dist/engine/post-scaffold.js +8 -0
  53. package/dist/engine/render-plan.d.ts +7 -0
  54. package/dist/engine/render-plan.js +9 -0
  55. package/dist/engine/template.d.ts +3 -0
  56. package/dist/engine/template.js +17 -0
  57. package/dist/engine/types.d.ts +2 -0
  58. package/dist/engine/types.js +8 -0
  59. package/dist/engine/workspace.d.ts +3 -0
  60. package/dist/engine/workspace.js +13 -0
  61. package/dist/index.d.ts +228 -152
  62. package/dist/index.js +144 -14
  63. package/dist/manifest.d.ts +71 -0
  64. package/dist/manifest.js +16 -0
  65. package/dist/output-mode.d.ts +2 -0
  66. package/dist/output-mode.js +10 -0
  67. package/dist/shared/chunk-b0y0cwkr.js +5533 -0
  68. package/dist/shared/outfitter-193jvzg4.d.ts +5 -0
  69. package/dist/shared/outfitter-1dd0k853.js +194 -0
  70. package/dist/shared/outfitter-1h7k8xxt.js +29 -0
  71. package/dist/shared/outfitter-1qwpjt6w.js +125 -0
  72. package/dist/shared/outfitter-2ngep1h2.d.ts +5 -0
  73. package/dist/shared/outfitter-2np85etz.js +95 -0
  74. package/dist/shared/outfitter-33w361tc.d.ts +18 -0
  75. package/dist/shared/outfitter-344t1r38.js +1 -0
  76. package/dist/shared/outfitter-3weh61w7.d.ts +25 -0
  77. package/dist/shared/outfitter-4s9meh3j.js +221 -0
  78. package/dist/shared/outfitter-6a4bq054.js +322 -0
  79. package/dist/shared/outfitter-6bkqjk86.d.ts +3 -0
  80. package/dist/shared/outfitter-6gc3g5wk.js +98 -0
  81. package/dist/shared/outfitter-7cv5fg1m.js +61 -0
  82. package/dist/shared/outfitter-7ha7p61k.d.ts +6 -0
  83. package/dist/shared/outfitter-7r12fj7f.js +30 -0
  84. package/dist/shared/outfitter-8y2dfx6n.js +11 -0
  85. package/dist/shared/outfitter-9c8edfsn.js +715 -0
  86. package/dist/shared/outfitter-9x1brcmq.js +184 -0
  87. package/dist/shared/outfitter-a79xrm12.d.ts +17 -0
  88. package/dist/shared/outfitter-amc4jbs1.d.ts +50 -0
  89. package/dist/shared/outfitter-ara3djt0.js +73 -0
  90. package/dist/shared/outfitter-avhm5z6w.js +82 -0
  91. package/dist/shared/outfitter-b5nd42y4.d.ts +45 -0
  92. package/dist/shared/outfitter-dd0btgec.d.ts +40 -0
  93. package/dist/shared/outfitter-e2zz5wv7.d.ts +51 -0
  94. package/dist/shared/outfitter-ehp18x1n.js +1 -0
  95. package/dist/shared/outfitter-fnsmx3xg.js +750 -0
  96. package/dist/shared/outfitter-gdvm5c0b.d.ts +4 -0
  97. package/dist/shared/outfitter-gp4v5gkf.js +322 -0
  98. package/dist/shared/outfitter-h1mnzzd1.d.ts +14 -0
  99. package/dist/shared/outfitter-hpymx4m9.js +184 -0
  100. package/dist/shared/outfitter-hvsaxgcp.js +1 -0
  101. package/dist/shared/outfitter-j8yc7294.d.ts +22 -0
  102. package/dist/shared/outfitter-jyxwznk1.js +404 -0
  103. package/dist/shared/outfitter-k112c427.js +21 -0
  104. package/dist/shared/outfitter-k56rmt24.d.ts +30 -0
  105. package/dist/shared/outfitter-ksa1pp4t.d.ts +4 -0
  106. package/dist/shared/outfitter-mdt37hqm.js +4 -0
  107. package/dist/shared/outfitter-mtbpabf3.js +91 -0
  108. package/dist/shared/outfitter-nm4m0v6x.d.ts +131 -0
  109. package/dist/shared/outfitter-nmeecf1b.js +531 -0
  110. package/dist/shared/outfitter-npyfbdmc.d.ts +6 -0
  111. package/dist/shared/outfitter-pxt58tsq.js +582 -0
  112. package/dist/shared/outfitter-q9agarmb.js +42 -0
  113. package/dist/shared/outfitter-qfgj5xpq.js +70 -0
  114. package/dist/shared/outfitter-qfh36ddg.d.ts +66 -0
  115. package/dist/shared/outfitter-s6k8y2p4.js +269 -0
  116. package/dist/shared/outfitter-sftf1s26.js +199 -0
  117. package/dist/shared/outfitter-sg7ncy4a.d.ts +51 -0
  118. package/dist/shared/outfitter-sgtq57qr.d.ts +5 -0
  119. package/dist/shared/outfitter-txre6cdn.d.ts +60 -0
  120. package/dist/shared/outfitter-vh4xgb93.js +35 -0
  121. package/dist/shared/outfitter-ya44h1km.js +191 -0
  122. package/dist/shared/outfitter-zwyvewr1.js +36 -0
  123. package/dist/targets/index.d.ts +4 -0
  124. package/dist/targets/index.js +29 -0
  125. package/dist/targets/registry.d.ts +3 -0
  126. package/dist/targets/registry.js +28 -0
  127. package/dist/targets/types.d.ts +2 -0
  128. package/dist/targets/types.js +1 -0
  129. package/package.json +154 -37
  130. package/templates/minimal/.gitignore.template +30 -0
  131. package/templates/minimal/.lefthook.yml.template +26 -0
  132. package/templates/minimal/package.json.template +46 -0
  133. package/templates/minimal/src/index.ts.template +26 -0
  134. package/templates/minimal/tsconfig.json.template +34 -0
  135. package/dist/shared/chunk-sak1tt33.js +0 -3457
@@ -1,3457 +0,0 @@
1
- // src/create/presets.ts
2
- var CREATE_PRESETS = {
3
- basic: {
4
- id: "basic",
5
- template: "basic",
6
- summary: "Minimal Bun + TypeScript project.",
7
- defaultBlocks: ["scaffolding"]
8
- },
9
- cli: {
10
- id: "cli",
11
- template: "cli",
12
- summary: "CLI starter with Outfitter command ergonomics.",
13
- defaultBlocks: ["scaffolding"]
14
- },
15
- daemon: {
16
- id: "daemon",
17
- template: "daemon",
18
- summary: "Daemon + control CLI starter.",
19
- defaultBlocks: ["scaffolding"]
20
- },
21
- mcp: {
22
- id: "mcp",
23
- template: "mcp",
24
- summary: "MCP server starter.",
25
- defaultBlocks: ["scaffolding"]
26
- }
27
- };
28
- var CREATE_PRESET_IDS = Object.keys(CREATE_PRESETS);
29
- function getCreatePreset(id) {
30
- const presets = CREATE_PRESETS;
31
- return presets[id];
32
- }
33
-
34
- // src/create/planner.ts
35
- import { Result, ValidationError } from "@outfitter/contracts";
36
- function derivePackageName(input) {
37
- return (input.packageName ?? input.name).trim();
38
- }
39
- function deriveProjectName(packageName) {
40
- if (!packageName.startsWith("@")) {
41
- return packageName.trim();
42
- }
43
- const scopeSeparator = packageName.indexOf("/");
44
- if (scopeSeparator < 0) {
45
- return "";
46
- }
47
- return packageName.slice(scopeSeparator + 1).trim();
48
- }
49
- function deriveBinName(projectName) {
50
- return projectName.toLowerCase().replace(/\s+/g, "-");
51
- }
52
- function planCreateProject(input) {
53
- const packageName = derivePackageName(input);
54
- if (packageName.length === 0) {
55
- return Result.err(new ValidationError({
56
- message: "Project name must not be empty",
57
- field: "name"
58
- }));
59
- }
60
- const targetDir = input.targetDir.trim();
61
- if (targetDir.length === 0) {
62
- return Result.err(new ValidationError({
63
- message: "Target directory must not be empty",
64
- field: "targetDir"
65
- }));
66
- }
67
- const projectName = deriveProjectName(packageName);
68
- if (projectName.length === 0) {
69
- return Result.err(new ValidationError({
70
- message: "Could not derive a project name from package name",
71
- field: "packageName"
72
- }));
73
- }
74
- const preset = getCreatePreset(input.preset);
75
- if (!preset) {
76
- return Result.err(new ValidationError({
77
- message: `Unknown create preset '${input.preset}'`,
78
- field: "preset"
79
- }));
80
- }
81
- const includeTooling = input.includeTooling ?? true;
82
- const defaultBlocks = includeTooling ? [...preset.defaultBlocks] : [];
83
- const changes = [
84
- {
85
- type: "copy-template",
86
- template: preset.template,
87
- targetDir,
88
- overlayBaseTemplate: true
89
- },
90
- { type: "inject-shared-config" }
91
- ];
92
- if (input.local) {
93
- changes.push({ type: "rewrite-local-dependencies", mode: "workspace" });
94
- }
95
- if (defaultBlocks.length > 0) {
96
- changes.push({ type: "add-blocks", blocks: defaultBlocks });
97
- }
98
- const plan = {
99
- preset,
100
- values: {
101
- packageName,
102
- projectName,
103
- version: input.version?.trim() || "0.1.0",
104
- description: input.description?.trim() || "A new project created with Outfitter",
105
- binName: deriveBinName(projectName),
106
- year: input.year ?? String(new Date().getFullYear())
107
- },
108
- changes
109
- };
110
- return Result.ok(plan);
111
- }
112
- // src/commands/create.ts
113
- import {
114
- existsSync as existsSync2,
115
- mkdirSync as mkdirSync2,
116
- readdirSync,
117
- readFileSync as readFileSync2,
118
- statSync,
119
- writeFileSync as writeFileSync2
120
- } from "node:fs";
121
- import { basename, dirname as dirname2, extname, join as join2, resolve as resolve2 } from "node:path";
122
- import { fileURLToPath as fileURLToPath2 } from "node:url";
123
- import {
124
- cancel,
125
- confirm,
126
- intro,
127
- isCancel,
128
- outro,
129
- select,
130
- text
131
- } from "@clack/prompts";
132
- import { exitWithError, output as output2 } from "@outfitter/cli/output";
133
- import { Result as Result3 } from "@outfitter/contracts";
134
-
135
- // src/commands/add.ts
136
- import {
137
- chmodSync,
138
- existsSync,
139
- mkdirSync,
140
- readFileSync,
141
- writeFileSync
142
- } from "node:fs";
143
- import { dirname, join, resolve } from "node:path";
144
- import { fileURLToPath } from "node:url";
145
- import { output } from "@outfitter/cli/output";
146
- import { Result as Result2 } from "@outfitter/contracts";
147
- import { RegistrySchema } from "@outfitter/tooling";
148
-
149
- class AddError extends Error {
150
- _tag = "AddError";
151
- constructor(message) {
152
- super(message);
153
- this.name = "AddError";
154
- }
155
- }
156
- function getRegistryPath() {
157
- let currentDir = dirname(fileURLToPath(import.meta.url));
158
- for (let i = 0;i < 10; i++) {
159
- const registryPath = join(currentDir, "node_modules/@outfitter/tooling/registry/registry.json");
160
- if (existsSync(registryPath)) {
161
- return registryPath;
162
- }
163
- const monoRepoPath = join(currentDir, "packages/tooling/registry/registry.json");
164
- if (existsSync(monoRepoPath)) {
165
- return monoRepoPath;
166
- }
167
- currentDir = dirname(currentDir);
168
- }
169
- throw new AddError("Could not find registry.json. Ensure @outfitter/tooling is installed.");
170
- }
171
- function loadRegistry() {
172
- try {
173
- const registryPath = getRegistryPath();
174
- const content = readFileSync(registryPath, "utf-8");
175
- const parsed = JSON.parse(content);
176
- const registry = RegistrySchema.parse(parsed);
177
- return Result2.ok(registry);
178
- } catch (error) {
179
- const message = error instanceof Error ? error.message : "Unknown error";
180
- return Result2.err(new AddError(`Failed to load registry: ${message}`));
181
- }
182
- }
183
- function resolveBlock(registry, blockName, visited = new Set) {
184
- if (visited.has(blockName)) {
185
- return Result2.err(new AddError(`Circular dependency detected for block: ${blockName}`));
186
- }
187
- visited.add(blockName);
188
- const block = registry.blocks[blockName];
189
- if (!block) {
190
- const available = Object.keys(registry.blocks).join(", ");
191
- return Result2.err(new AddError(`Block '${blockName}' not found. Available blocks: ${available}`));
192
- }
193
- if (block.extends && block.extends.length > 0) {
194
- const mergedFiles = [];
195
- const mergedDeps = {};
196
- const mergedDevDeps = {};
197
- for (const extendedName of block.extends) {
198
- const extendedResult = resolveBlock(registry, extendedName, visited);
199
- if (extendedResult.isErr()) {
200
- return extendedResult;
201
- }
202
- const extended = extendedResult.value;
203
- if (extended.files) {
204
- mergedFiles.push(...extended.files);
205
- }
206
- if (extended.dependencies) {
207
- Object.assign(mergedDeps, extended.dependencies);
208
- }
209
- if (extended.devDependencies) {
210
- Object.assign(mergedDevDeps, extended.devDependencies);
211
- }
212
- }
213
- if (block.files) {
214
- mergedFiles.push(...block.files);
215
- }
216
- if (block.dependencies) {
217
- Object.assign(mergedDeps, block.dependencies);
218
- }
219
- if (block.devDependencies) {
220
- Object.assign(mergedDevDeps, block.devDependencies);
221
- }
222
- return Result2.ok({
223
- name: block.name,
224
- description: block.description,
225
- files: mergedFiles.length > 0 ? mergedFiles : undefined,
226
- dependencies: Object.keys(mergedDeps).length > 0 ? mergedDeps : undefined,
227
- devDependencies: Object.keys(mergedDevDeps).length > 0 ? mergedDevDeps : undefined
228
- });
229
- }
230
- return Result2.ok(block);
231
- }
232
- function writeFile(filePath, content, executable) {
233
- const dir = dirname(filePath);
234
- if (!existsSync(dir)) {
235
- mkdirSync(dir, { recursive: true });
236
- }
237
- writeFileSync(filePath, content, "utf-8");
238
- if (executable) {
239
- chmodSync(filePath, 493);
240
- }
241
- }
242
- function updatePackageJson(cwd, dependencies, devDependencies, dryRun) {
243
- const packageJsonPath = join(cwd, "package.json");
244
- if (!existsSync(packageJsonPath)) {
245
- return { dependencies, devDependencies };
246
- }
247
- const content = readFileSync(packageJsonPath, "utf-8");
248
- const pkg = JSON.parse(content);
249
- const existingDeps = pkg["dependencies"] ?? {};
250
- const existingDevDeps = pkg["devDependencies"] ?? {};
251
- const addedDeps = {};
252
- const addedDevDeps = {};
253
- for (const [name, version] of Object.entries(dependencies)) {
254
- if (!existingDeps[name]) {
255
- existingDeps[name] = version;
256
- addedDeps[name] = version;
257
- }
258
- }
259
- for (const [name, version] of Object.entries(devDependencies)) {
260
- if (!(existingDevDeps[name] || existingDeps[name])) {
261
- existingDevDeps[name] = version;
262
- addedDevDeps[name] = version;
263
- }
264
- }
265
- if (!dryRun && (Object.keys(addedDeps).length > 0 || Object.keys(addedDevDeps).length > 0)) {
266
- if (Object.keys(existingDeps).length > 0) {
267
- pkg["dependencies"] = existingDeps;
268
- }
269
- if (Object.keys(existingDevDeps).length > 0) {
270
- pkg["devDependencies"] = existingDevDeps;
271
- }
272
- writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
273
- `);
274
- }
275
- return { dependencies: addedDeps, devDependencies: addedDevDeps };
276
- }
277
- async function runAdd(input) {
278
- const { block: blockName, force, dryRun, cwd = process.cwd() } = input;
279
- const resolvedCwd = resolve(cwd);
280
- const registryResult = loadRegistry();
281
- if (registryResult.isErr()) {
282
- return registryResult;
283
- }
284
- const registry = registryResult.value;
285
- const blockResult = resolveBlock(registry, blockName);
286
- if (blockResult.isErr()) {
287
- return blockResult;
288
- }
289
- const block = blockResult.value;
290
- const created = [];
291
- const skipped = [];
292
- const overwritten = [];
293
- if (block.files) {
294
- for (const file of block.files) {
295
- const targetPath = join(resolvedCwd, file.path);
296
- const fileExists = existsSync(targetPath);
297
- if (fileExists && !force) {
298
- skipped.push(file.path);
299
- continue;
300
- }
301
- if (!dryRun) {
302
- writeFile(targetPath, file.content, file.executable ?? false);
303
- }
304
- if (fileExists) {
305
- overwritten.push(file.path);
306
- } else {
307
- created.push(file.path);
308
- }
309
- }
310
- }
311
- const { dependencies, devDependencies } = updatePackageJson(resolvedCwd, block.dependencies ?? {}, block.devDependencies ?? {}, dryRun);
312
- return Result2.ok({
313
- created,
314
- skipped,
315
- overwritten,
316
- dependencies,
317
- devDependencies
318
- });
319
- }
320
- async function printAddResults(result, dryRun, options) {
321
- const mode = options?.mode;
322
- if (mode === "json" || mode === "jsonl") {
323
- await output({
324
- dryRun,
325
- created: result.created,
326
- overwritten: result.overwritten,
327
- skipped: result.skipped,
328
- dependencies: result.dependencies,
329
- devDependencies: result.devDependencies
330
- }, { mode });
331
- return;
332
- }
333
- const lines = [];
334
- const prefix = dryRun ? "[dry-run] Would " : "";
335
- if (result.created.length > 0) {
336
- lines.push(`${prefix}create ${result.created.length} file(s):`);
337
- for (const file of result.created) {
338
- lines.push(` ✓ ${file}`);
339
- }
340
- }
341
- if (result.overwritten.length > 0) {
342
- lines.push(`${prefix}overwrite ${result.overwritten.length} file(s):`);
343
- for (const file of result.overwritten) {
344
- lines.push(` ✓ ${file}`);
345
- }
346
- }
347
- if (result.skipped.length > 0) {
348
- lines.push(`Skipped ${result.skipped.length} existing file(s):`);
349
- for (const file of result.skipped) {
350
- lines.push(` - ${file} (use --force to overwrite)`);
351
- }
352
- }
353
- const depCount = Object.keys(result.dependencies).length + Object.keys(result.devDependencies).length;
354
- if (depCount > 0) {
355
- lines.push("", `${prefix}add ${depCount} package(s) to package.json:`);
356
- for (const [name, version] of Object.entries(result.dependencies)) {
357
- lines.push(` + ${name}@${version}`);
358
- }
359
- for (const [name, version] of Object.entries(result.devDependencies)) {
360
- lines.push(` + ${name}@${version} (dev)`);
361
- }
362
- if (!dryRun) {
363
- lines.push("", "Run `bun install` to install new dependencies.");
364
- }
365
- }
366
- await output(lines);
367
- }
368
- function listBlocks() {
369
- const registryResult = loadRegistry();
370
- if (registryResult.isErr()) {
371
- return registryResult;
372
- }
373
- const blocks = Object.keys(registryResult.value.blocks);
374
- return Result2.ok(blocks);
375
- }
376
-
377
- // src/commands/shared-deps.ts
378
- var SHARED_DEV_DEPS = {
379
- "@biomejs/biome": "^2.3.12",
380
- "@outfitter/tooling": "^0.2.1",
381
- "@types/bun": "^1.3.7",
382
- lefthook: "^2.0.16",
383
- typescript: "^5.9.3",
384
- ultracite: "^7.1.1"
385
- };
386
- var SHARED_SCRIPTS = {
387
- check: "ultracite check",
388
- "clean:artifacts": "rm -rf dist .turbo",
389
- "verify:ci": "bun run typecheck && bun run check && bun run build && bun run test",
390
- lint: "biome check .",
391
- "lint:fix": "biome check . --write",
392
- format: "biome format --write .",
393
- typecheck: "tsc --noEmit"
394
- };
395
-
396
- // src/commands/create.ts
397
- class CreateError extends Error {
398
- _tag = "CreateError";
399
- constructor(message) {
400
- super(message);
401
- this.name = "CreateError";
402
- }
403
- }
404
- var BINARY_EXTENSIONS = new Set([
405
- ".png",
406
- ".jpg",
407
- ".jpeg",
408
- ".gif",
409
- ".ico",
410
- ".webp",
411
- ".bmp",
412
- ".tiff",
413
- ".svg",
414
- ".woff",
415
- ".woff2",
416
- ".ttf",
417
- ".otf",
418
- ".eot",
419
- ".mp3",
420
- ".mp4",
421
- ".wav",
422
- ".ogg",
423
- ".webm",
424
- ".zip",
425
- ".tar",
426
- ".gz",
427
- ".bz2",
428
- ".7z",
429
- ".pdf",
430
- ".exe",
431
- ".dll",
432
- ".so",
433
- ".dylib",
434
- ".node",
435
- ".wasm",
436
- ".bin",
437
- ".dat",
438
- ".db",
439
- ".sqlite",
440
- ".sqlite3"
441
- ]);
442
- var DEPENDENCY_SECTIONS = [
443
- "dependencies",
444
- "devDependencies",
445
- "peerDependencies",
446
- "optionalDependencies"
447
- ];
448
- function resolveYear() {
449
- return String(new Date().getFullYear());
450
- }
451
- function parseBlocks(withFlag) {
452
- if (!withFlag) {
453
- return;
454
- }
455
- const blocks = withFlag.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
456
- return blocks.length > 0 ? blocks : undefined;
457
- }
458
- function deriveProjectName2(packageName) {
459
- if (packageName.startsWith("@")) {
460
- const parts = packageName.split("/");
461
- if (parts.length > 1 && parts[1]) {
462
- return parts[1];
463
- }
464
- }
465
- return packageName;
466
- }
467
- function getTemplatesDir() {
468
- let currentDir = dirname2(fileURLToPath2(import.meta.url));
469
- for (let i = 0;i < 10; i++) {
470
- const templatesPath = join2(currentDir, "templates");
471
- if (existsSync2(templatesPath)) {
472
- return templatesPath;
473
- }
474
- currentDir = dirname2(currentDir);
475
- }
476
- return join2(process.cwd(), "templates");
477
- }
478
- function getOutputFilename(templateFilename) {
479
- if (templateFilename.endsWith(".template")) {
480
- return templateFilename.slice(0, -".template".length);
481
- }
482
- return templateFilename;
483
- }
484
- function isBinaryFile(filename) {
485
- return BINARY_EXTENSIONS.has(extname(filename).toLowerCase());
486
- }
487
- function replacePlaceholders(content, values) {
488
- return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
489
- if (Object.hasOwn(values, key)) {
490
- return values[key];
491
- }
492
- return match;
493
- });
494
- }
495
- function copyTemplateFiles(templateDir, targetDir, values, force, allowOverwrite = false, overwritablePaths, writtenPaths) {
496
- try {
497
- if (!existsSync2(targetDir)) {
498
- mkdirSync2(targetDir, { recursive: true });
499
- }
500
- const entries = readdirSync(templateDir);
501
- for (const entry of entries) {
502
- const sourcePath = join2(templateDir, entry);
503
- const sourceStat = statSync(sourcePath);
504
- if (sourceStat.isDirectory()) {
505
- const targetSubDir = join2(targetDir, entry);
506
- const nestedResult = copyTemplateFiles(sourcePath, targetSubDir, values, force, allowOverwrite, overwritablePaths, writtenPaths);
507
- if (nestedResult.isErr()) {
508
- return nestedResult;
509
- }
510
- continue;
511
- }
512
- if (!sourceStat.isFile()) {
513
- continue;
514
- }
515
- const outputFilename = getOutputFilename(entry);
516
- const targetPath = join2(targetDir, outputFilename);
517
- const targetExists = existsSync2(targetPath);
518
- const canOverlay = allowOverwrite && (!targetExists || Boolean(overwritablePaths?.has(targetPath)));
519
- if (targetExists && !force && !canOverlay) {
520
- return Result3.err(new CreateError(`File '${targetPath}' already exists. Use --force to overwrite.`));
521
- }
522
- if (isBinaryFile(outputFilename)) {
523
- const buffer = readFileSync2(sourcePath);
524
- writeFileSync2(targetPath, buffer);
525
- writtenPaths?.add(targetPath);
526
- continue;
527
- }
528
- const content = readFileSync2(sourcePath, "utf-8");
529
- const processedContent = replacePlaceholders(content, values);
530
- writeFileSync2(targetPath, processedContent, "utf-8");
531
- writtenPaths?.add(targetPath);
532
- }
533
- return Result3.ok(undefined);
534
- } catch (error) {
535
- const message = error instanceof Error ? error.message : "Unknown error";
536
- return Result3.err(new CreateError(`Failed to copy template files: ${message}`));
537
- }
538
- }
539
- function injectSharedConfig(targetDir) {
540
- const packageJsonPath = join2(targetDir, "package.json");
541
- if (!existsSync2(packageJsonPath)) {
542
- return Result3.ok(undefined);
543
- }
544
- try {
545
- const content = readFileSync2(packageJsonPath, "utf-8");
546
- const parsed = JSON.parse(content);
547
- const existingDevDeps = parsed["devDependencies"] ?? {};
548
- parsed["devDependencies"] = { ...SHARED_DEV_DEPS, ...existingDevDeps };
549
- const existingScripts = parsed["scripts"] ?? {};
550
- parsed["scripts"] = { ...SHARED_SCRIPTS, ...existingScripts };
551
- writeFileSync2(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
552
- `, "utf-8");
553
- return Result3.ok(undefined);
554
- } catch (error) {
555
- const message = error instanceof Error ? error.message : "Unknown error";
556
- return Result3.err(new CreateError(`Failed to inject shared config: ${message}`));
557
- }
558
- }
559
- function rewriteLocalDependencies(targetDir) {
560
- const packageJsonPath = join2(targetDir, "package.json");
561
- if (!existsSync2(packageJsonPath)) {
562
- return Result3.ok(undefined);
563
- }
564
- try {
565
- const content = readFileSync2(packageJsonPath, "utf-8");
566
- const parsed = JSON.parse(content);
567
- let updated = false;
568
- for (const section of DEPENDENCY_SECTIONS) {
569
- const deps = parsed[section];
570
- if (!deps || typeof deps !== "object" || Array.isArray(deps)) {
571
- continue;
572
- }
573
- const entries = deps;
574
- for (const [name, version] of Object.entries(entries)) {
575
- if (typeof version === "string" && name.startsWith("@outfitter/") && version !== "workspace:*") {
576
- entries[name] = "workspace:*";
577
- updated = true;
578
- }
579
- }
580
- }
581
- if (updated) {
582
- writeFileSync2(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
583
- `, "utf-8");
584
- }
585
- return Result3.ok(undefined);
586
- } catch (error) {
587
- const message = error instanceof Error ? error.message : "Unknown error";
588
- return Result3.err(new CreateError(`Failed to update local dependencies: ${message}`));
589
- }
590
- }
591
- async function addBlocks(targetDir, blocks, force) {
592
- const mergedResult = {
593
- created: [],
594
- skipped: [],
595
- overwritten: [],
596
- dependencies: {},
597
- devDependencies: {}
598
- };
599
- for (const blockName of blocks) {
600
- const result = await runAdd({
601
- block: blockName,
602
- force,
603
- dryRun: false,
604
- cwd: targetDir
605
- });
606
- if (result.isErr()) {
607
- return Result3.err(new CreateError(`Failed to add block '${blockName}': ${result.error.message}`));
608
- }
609
- mergedResult.created.push(...result.value.created);
610
- mergedResult.skipped.push(...result.value.skipped);
611
- mergedResult.overwritten.push(...result.value.overwritten);
612
- Object.assign(mergedResult.dependencies, result.value.dependencies);
613
- Object.assign(mergedResult.devDependencies, result.value.devDependencies);
614
- }
615
- return Result3.ok(mergedResult);
616
- }
617
- function findProjectTargetDir(plan) {
618
- const copyStep = plan.changes.find((change) => change.type === "copy-template");
619
- if (!copyStep) {
620
- return Result3.err(new CreateError("Planner output missing copy-template step"));
621
- }
622
- return Result3.ok(copyStep.targetDir);
623
- }
624
- function applyBlockOverrides(plan, blocksOverride) {
625
- if (!blocksOverride) {
626
- return plan;
627
- }
628
- const changesWithoutBlocks = plan.changes.filter((change) => change.type !== "add-blocks");
629
- if (blocksOverride.length === 0) {
630
- return {
631
- ...plan,
632
- changes: changesWithoutBlocks
633
- };
634
- }
635
- return {
636
- ...plan,
637
- changes: [
638
- ...changesWithoutBlocks,
639
- { type: "add-blocks", blocks: blocksOverride }
640
- ]
641
- };
642
- }
643
- async function executePlan(plan, force) {
644
- const targetDirResult = findProjectTargetDir(plan);
645
- if (targetDirResult.isErr()) {
646
- return targetDirResult;
647
- }
648
- const targetDir = targetDirResult.value;
649
- const templatesDir = getTemplatesDir();
650
- const values = {
651
- name: plan.values.projectName,
652
- projectName: plan.values.projectName,
653
- packageName: plan.values.packageName,
654
- binName: plan.values.binName,
655
- version: plan.values.version,
656
- description: plan.values.description,
657
- year: plan.values.year
658
- };
659
- let blocksAdded;
660
- for (const change of plan.changes) {
661
- if (change.type === "copy-template") {
662
- const templatePath = join2(templatesDir, change.template);
663
- if (!existsSync2(templatePath)) {
664
- return Result3.err(new CreateError(`Template '${change.template}' not found in ${templatesDir}`));
665
- }
666
- if (change.overlayBaseTemplate) {
667
- const basePath = join2(templatesDir, "_base");
668
- if (existsSync2(basePath)) {
669
- const baseWrittenPaths = new Set;
670
- const baseResult = copyTemplateFiles(basePath, targetDir, values, force, false, undefined, baseWrittenPaths);
671
- if (baseResult.isErr()) {
672
- return baseResult;
673
- }
674
- const templateResult2 = copyTemplateFiles(templatePath, targetDir, values, force, true, baseWrittenPaths);
675
- if (templateResult2.isErr()) {
676
- return templateResult2;
677
- }
678
- continue;
679
- }
680
- }
681
- const templateResult = copyTemplateFiles(templatePath, targetDir, values, force);
682
- if (templateResult.isErr()) {
683
- return templateResult;
684
- }
685
- continue;
686
- }
687
- if (change.type === "inject-shared-config") {
688
- const result = injectSharedConfig(targetDir);
689
- if (result.isErr()) {
690
- return result;
691
- }
692
- continue;
693
- }
694
- if (change.type === "rewrite-local-dependencies") {
695
- const result = rewriteLocalDependencies(targetDir);
696
- if (result.isErr()) {
697
- return result;
698
- }
699
- continue;
700
- }
701
- if (change.type === "add-blocks") {
702
- const result = await addBlocks(targetDir, change.blocks, force);
703
- if (result.isErr()) {
704
- return result;
705
- }
706
- blocksAdded = result.value;
707
- }
708
- }
709
- return Result3.ok(blocksAdded);
710
- }
711
- function buildWorkspaceRootPackageJson(workspaceName, projectDirName) {
712
- const packagePath = `packages/${projectDirName}`;
713
- const workspacePackage = {
714
- name: workspaceName,
715
- private: true,
716
- version: "0.1.0",
717
- workspaces: ["packages/*"],
718
- scripts: {
719
- build: `bun run --cwd ${packagePath} build`,
720
- dev: `bun run --cwd ${packagePath} dev`,
721
- test: `bun run --cwd ${packagePath} test`,
722
- typecheck: `bun run --cwd ${packagePath} typecheck`,
723
- lint: `bun run --cwd ${packagePath} lint`,
724
- "lint:fix": `bun run --cwd ${packagePath} lint:fix`,
725
- format: `bun run --cwd ${packagePath} format`
726
- }
727
- };
728
- return `${JSON.stringify(workspacePackage, null, 2)}
729
- `;
730
- }
731
- function scaffoldWorkspaceRoot(rootDir, workspaceName, projectDirName, force) {
732
- const packageJsonPath = join2(rootDir, "package.json");
733
- if (existsSync2(packageJsonPath) && !force) {
734
- return Result3.err(new CreateError(`Directory '${rootDir}' already has a package.json. Use --force to overwrite.`));
735
- }
736
- try {
737
- if (!existsSync2(rootDir)) {
738
- mkdirSync2(rootDir, { recursive: true });
739
- }
740
- mkdirSync2(join2(rootDir, "packages"), { recursive: true });
741
- writeFileSync2(packageJsonPath, buildWorkspaceRootPackageJson(workspaceName, projectDirName), "utf-8");
742
- const gitignorePath = join2(rootDir, ".gitignore");
743
- if (force || !existsSync2(gitignorePath)) {
744
- writeFileSync2(gitignorePath, `node_modules
745
- **/dist
746
- `, "utf-8");
747
- }
748
- return Result3.ok(undefined);
749
- } catch (error) {
750
- const message = error instanceof Error ? error.message : "Unknown error";
751
- return Result3.err(new CreateError(`Failed to scaffold workspace root: ${message}`));
752
- }
753
- }
754
- async function resolveInput(options) {
755
- const rootDir = resolve2(options.targetDir);
756
- const defaultName = basename(rootDir);
757
- if (options.yes) {
758
- const packageName2 = (options.name ?? defaultName).trim();
759
- if (packageName2.length === 0) {
760
- return Result3.err(new CreateError("Project name must not be empty"));
761
- }
762
- const structure = options.structure ?? "single";
763
- const blocksOverride2 = parseBlocks(options.with);
764
- const workspaceName2 = (options.workspaceName ?? defaultName).trim() || defaultName;
765
- return Result3.ok({
766
- rootDir,
767
- packageName: packageName2,
768
- preset: options.preset ?? "basic",
769
- structure,
770
- includeTooling: !(options.noTooling ?? false),
771
- local: Boolean(options.local),
772
- ...blocksOverride2 ? { blocksOverride: blocksOverride2 } : {},
773
- ...structure === "workspace" ? { workspaceName: workspaceName2 } : {}
774
- });
775
- }
776
- intro("Outfitter create");
777
- const packageNameValue = options.name ?? await text({
778
- message: "Project package name",
779
- placeholder: defaultName,
780
- initialValue: defaultName,
781
- validate: (value) => value.trim().length === 0 ? "Project name is required" : undefined
782
- });
783
- if (isCancel(packageNameValue)) {
784
- cancel("Create cancelled.");
785
- return Result3.err(new CreateError("Create cancelled"));
786
- }
787
- const presetValue = options.preset ?? await select({
788
- message: "Select a preset",
789
- options: CREATE_PRESET_IDS.map((id) => ({
790
- value: id,
791
- label: id,
792
- hint: CREATE_PRESETS[id].summary
793
- })),
794
- initialValue: "basic"
795
- });
796
- if (isCancel(presetValue)) {
797
- cancel("Create cancelled.");
798
- return Result3.err(new CreateError("Create cancelled"));
799
- }
800
- const structureValue = options.structure ?? await select({
801
- message: "Project structure",
802
- options: [
803
- {
804
- value: "single",
805
- label: "Single package",
806
- hint: "One package in the target directory"
807
- },
808
- {
809
- value: "workspace",
810
- label: "Workspace",
811
- hint: "Root workspace with project under packages/"
812
- }
813
- ],
814
- initialValue: "single"
815
- });
816
- if (isCancel(structureValue)) {
817
- cancel("Create cancelled.");
818
- return Result3.err(new CreateError("Create cancelled"));
819
- }
820
- const includeTooling = options.noTooling !== undefined ? !options.noTooling : await confirm({
821
- message: "Add default tooling blocks?",
822
- initialValue: true
823
- });
824
- if (isCancel(includeTooling)) {
825
- cancel("Create cancelled.");
826
- return Result3.err(new CreateError("Create cancelled"));
827
- }
828
- const localValue = options.local !== undefined ? options.local : await confirm({
829
- message: "Use workspace:* for @outfitter dependencies?",
830
- initialValue: false
831
- });
832
- if (isCancel(localValue)) {
833
- cancel("Create cancelled.");
834
- return Result3.err(new CreateError("Create cancelled"));
835
- }
836
- let workspaceName;
837
- if (structureValue === "workspace") {
838
- const workspaceNameValue = options.workspaceName ?? await text({
839
- message: "Workspace package name",
840
- placeholder: defaultName,
841
- initialValue: defaultName,
842
- validate: (value) => value.trim().length === 0 ? "Workspace name is required" : undefined
843
- });
844
- if (isCancel(workspaceNameValue)) {
845
- cancel("Create cancelled.");
846
- return Result3.err(new CreateError("Create cancelled"));
847
- }
848
- workspaceName = workspaceNameValue.trim();
849
- }
850
- outro("Scaffolding project...");
851
- const packageName = packageNameValue.trim();
852
- if (packageName.length === 0) {
853
- return Result3.err(new CreateError("Project name must not be empty"));
854
- }
855
- const blocksOverride = parseBlocks(options.with);
856
- return Result3.ok({
857
- rootDir,
858
- packageName,
859
- preset: presetValue,
860
- structure: structureValue,
861
- includeTooling,
862
- local: Boolean(localValue),
863
- ...blocksOverride ? { blocksOverride } : {},
864
- ...workspaceName ? { workspaceName } : {}
865
- });
866
- }
867
- async function runCreate(options) {
868
- const inputResult = await resolveInput(options);
869
- if (inputResult.isErr()) {
870
- return inputResult;
871
- }
872
- const input = inputResult.value;
873
- const projectDir = input.structure === "workspace" ? join2(input.rootDir, "packages", deriveProjectName2(input.packageName)) : input.rootDir;
874
- const planResult = planCreateProject({
875
- name: input.packageName,
876
- targetDir: projectDir,
877
- preset: input.preset,
878
- includeTooling: input.includeTooling,
879
- local: input.local,
880
- year: resolveYear()
881
- });
882
- if (planResult.isErr()) {
883
- return Result3.err(new CreateError(planResult.error.message));
884
- }
885
- const plan = applyBlockOverrides(planResult.value, input.blocksOverride);
886
- if (input.structure === "workspace") {
887
- const workspaceName = input.workspaceName && input.workspaceName.length > 0 ? input.workspaceName : basename(input.rootDir);
888
- const workspaceResult = scaffoldWorkspaceRoot(input.rootDir, workspaceName, deriveProjectName2(input.packageName), options.force);
889
- if (workspaceResult.isErr()) {
890
- return workspaceResult;
891
- }
892
- } else if (existsSync2(join2(input.rootDir, "package.json")) && !options.force) {
893
- return Result3.err(new CreateError(`Directory '${input.rootDir}' already has a package.json. Use --force to overwrite.`));
894
- }
895
- const executeResult = await executePlan(plan, options.force);
896
- if (executeResult.isErr()) {
897
- return executeResult;
898
- }
899
- return Result3.ok({
900
- structure: input.structure,
901
- rootDir: input.rootDir,
902
- projectDir,
903
- preset: input.preset,
904
- packageName: input.packageName,
905
- blocksAdded: executeResult.value
906
- });
907
- }
908
- async function printCreateResults(result, options) {
909
- const mode = options?.mode;
910
- if (mode === "json" || mode === "jsonl") {
911
- await output2({
912
- structure: result.structure,
913
- rootDir: result.rootDir,
914
- projectDir: result.projectDir,
915
- preset: result.preset,
916
- packageName: result.packageName,
917
- blocksAdded: result.blocksAdded ?? null,
918
- nextSteps: ["bun install", "bun run build", "bun run test"]
919
- }, { mode });
920
- return;
921
- }
922
- const lines = [
923
- `Project created successfully in ${result.rootDir}`,
924
- `Structure: ${result.structure}`,
925
- `Preset: ${result.preset}`
926
- ];
927
- if (result.structure === "workspace") {
928
- lines.push(`Workspace project path: ${result.projectDir}`);
929
- }
930
- if (result.blocksAdded) {
931
- if (result.blocksAdded.created.length > 0) {
932
- lines.push("", `Added ${result.blocksAdded.created.length} tooling file(s):`);
933
- for (const file of result.blocksAdded.created) {
934
- lines.push(` ✓ ${file}`);
935
- }
936
- }
937
- if (result.blocksAdded.skipped.length > 0) {
938
- lines.push("", `Skipped ${result.blocksAdded.skipped.length} existing file(s):`);
939
- for (const file of result.blocksAdded.skipped) {
940
- lines.push(` - ${file}`);
941
- }
942
- }
943
- }
944
- lines.push("", "Next steps:");
945
- if (result.structure === "workspace") {
946
- lines.push(" bun install", ` bun run --cwd ${result.projectDir} dev`);
947
- } else {
948
- lines.push(" bun install", " bun run dev");
949
- }
950
- await output2(lines);
951
- }
952
- function createCommand(program) {
953
- program.command("create [directory]").description("Interactive scaffolding flow for Outfitter projects").option("-n, --name <name>", "Package name").option("-p, --preset <preset>", `Preset to scaffold (${CREATE_PRESET_IDS.join(", ")})`).option("-s, --structure <mode>", "Project structure (single|workspace)").option("--workspace-name <name>", "Workspace root package name").option("--local", "Use workspace:* for @outfitter dependencies", false).option("--workspace", "Alias for --local", false).option("-f, --force", "Overwrite existing files", false).option("--with <blocks>", "Comma-separated tooling blocks to add").option("--no-tooling", "Skip default tooling blocks").option("-y, --yes", "Skip prompts and use defaults", false).action(async (targetDir, flags) => {
954
- const result = await runCreate({
955
- targetDir: targetDir ?? process.cwd(),
956
- name: flags.name,
957
- preset: flags.preset,
958
- structure: flags.structure,
959
- workspaceName: flags.workspaceName,
960
- local: Boolean(flags.local || flags.workspace),
961
- force: Boolean(flags.force),
962
- with: flags.with,
963
- noTooling: flags.noTooling,
964
- yes: Boolean(flags.yes)
965
- });
966
- if (result.isErr()) {
967
- exitWithError(result.error);
968
- return;
969
- }
970
- await printCreateResults(result.value);
971
- });
972
- }
973
-
974
- // src/commands/doctor.ts
975
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
976
- import { join as join3, resolve as resolve3 } from "node:path";
977
- import { output as output3 } from "@outfitter/cli/output";
978
- import { createTheme } from "@outfitter/cli/render";
979
- var MIN_BUN_VERSION = "1.3.6";
980
- function checkBunVersion() {
981
- const required = MIN_BUN_VERSION;
982
- const current = Bun.version;
983
- const passed = Bun.semver.satisfies(current, `>=${required}`);
984
- return {
985
- passed,
986
- version: current,
987
- required,
988
- ...passed ? {} : {
989
- error: `Bun version ${current} is below minimum required ${required}`
990
- }
991
- };
992
- }
993
- function checkPackageJson(cwd) {
994
- const packageJsonPath = join3(cwd, "package.json");
995
- if (!existsSync3(packageJsonPath)) {
996
- return {
997
- passed: false,
998
- error: "package.json not found in current directory"
999
- };
1000
- }
1001
- try {
1002
- const content = readFileSync3(packageJsonPath, "utf-8");
1003
- const parsed = JSON.parse(content);
1004
- if (typeof parsed["name"] !== "string" || parsed["name"].length === 0) {
1005
- return {
1006
- passed: false,
1007
- error: "package.json is missing required 'name' field"
1008
- };
1009
- }
1010
- if (typeof parsed["version"] !== "string" || parsed["version"].length === 0) {
1011
- return {
1012
- passed: false,
1013
- error: "package.json is missing required 'version' field"
1014
- };
1015
- }
1016
- return {
1017
- passed: true,
1018
- name: parsed["name"],
1019
- version: parsed["version"]
1020
- };
1021
- } catch (error) {
1022
- const message = error instanceof Error ? error.message : "Unknown error";
1023
- return {
1024
- passed: false,
1025
- error: `package.json is invalid JSON: ${message}`
1026
- };
1027
- }
1028
- }
1029
- function checkDependencies(cwd) {
1030
- const packageJsonPath = join3(cwd, "package.json");
1031
- if (!existsSync3(packageJsonPath)) {
1032
- return { passed: true, count: 0 };
1033
- }
1034
- try {
1035
- const content = readFileSync3(packageJsonPath, "utf-8");
1036
- const parsed = JSON.parse(content);
1037
- const dependencies = parsed["dependencies"];
1038
- const devDependencies = parsed["devDependencies"];
1039
- const allDeps = [
1040
- ...Object.keys(dependencies ?? {}),
1041
- ...Object.keys(devDependencies ?? {})
1042
- ];
1043
- if (allDeps.length === 0) {
1044
- return { passed: true, count: 0 };
1045
- }
1046
- const nodeModulesPath = join3(cwd, "node_modules");
1047
- if (!existsSync3(nodeModulesPath)) {
1048
- return {
1049
- passed: false,
1050
- count: allDeps.length,
1051
- error: "node_modules not found. Run 'bun install' to install dependencies."
1052
- };
1053
- }
1054
- const missing = [];
1055
- for (const dep of allDeps) {
1056
- const depPath = join3(nodeModulesPath, dep);
1057
- if (!existsSync3(depPath)) {
1058
- missing.push(dep);
1059
- }
1060
- }
1061
- if (missing.length > 0) {
1062
- return {
1063
- passed: false,
1064
- count: allDeps.length,
1065
- missing,
1066
- error: `Missing dependencies: ${missing.join(", ")}. Run 'bun install' to install.`
1067
- };
1068
- }
1069
- return { passed: true, count: allDeps.length };
1070
- } catch {
1071
- return { passed: true, count: 0 };
1072
- }
1073
- }
1074
- function checkConfigFiles(cwd) {
1075
- return {
1076
- tsconfig: existsSync3(join3(cwd, "tsconfig.json")),
1077
- biome: existsSync3(join3(cwd, "biome.json"))
1078
- };
1079
- }
1080
- function checkDirectories(cwd) {
1081
- return {
1082
- src: existsSync3(join3(cwd, "src")),
1083
- tests: existsSync3(join3(cwd, "src", "__tests__")) || existsSync3(join3(cwd, "tests"))
1084
- };
1085
- }
1086
- function runDoctor(options) {
1087
- const cwd = resolve3(options.cwd);
1088
- const bunVersion = checkBunVersion();
1089
- const packageJson = checkPackageJson(cwd);
1090
- const dependencies = checkDependencies(cwd);
1091
- const configFiles = checkConfigFiles(cwd);
1092
- const directories = checkDirectories(cwd);
1093
- const checkResults = [
1094
- bunVersion.passed,
1095
- packageJson.passed,
1096
- dependencies.passed,
1097
- configFiles.tsconfig,
1098
- directories.src
1099
- ];
1100
- const passed = checkResults.filter(Boolean).length;
1101
- const failed = checkResults.length - passed;
1102
- const total = checkResults.length;
1103
- return {
1104
- checks: {
1105
- bunVersion,
1106
- packageJson,
1107
- dependencies,
1108
- configFiles,
1109
- directories
1110
- },
1111
- summary: { passed, failed, total },
1112
- exitCode: failed > 0 ? 1 : 0
1113
- };
1114
- }
1115
- async function printDoctorResults(result, options) {
1116
- const mode = options?.mode;
1117
- if (mode === "json" || mode === "jsonl") {
1118
- await output3(result, { mode });
1119
- return;
1120
- }
1121
- const theme = createTheme();
1122
- const lines = ["", "Outfitter Doctor", "", "=".repeat(50)];
1123
- const bunIcon = result.checks.bunVersion.passed ? theme.success("[PASS]") : theme.error("[FAIL]");
1124
- lines.push(`${bunIcon} Bun Version: ${result.checks.bunVersion.version} (requires ${result.checks.bunVersion.required})`);
1125
- if (result.checks.bunVersion.error) {
1126
- lines.push(` ${theme.muted(result.checks.bunVersion.error)}`);
1127
- }
1128
- const pkgIcon = result.checks.packageJson.passed ? theme.success("[PASS]") : theme.error("[FAIL]");
1129
- lines.push(`${pkgIcon} package.json`);
1130
- if (result.checks.packageJson.error) {
1131
- lines.push(` ${theme.muted(result.checks.packageJson.error)}`);
1132
- } else if (result.checks.packageJson.name) {
1133
- lines.push(` ${theme.muted(`${result.checks.packageJson.name}@${result.checks.packageJson.version}`)}`);
1134
- }
1135
- const depsIcon = result.checks.dependencies.passed ? theme.success("[PASS]") : theme.error("[FAIL]");
1136
- lines.push(`${depsIcon} Dependencies`);
1137
- if (result.checks.dependencies.error) {
1138
- lines.push(` ${theme.muted(result.checks.dependencies.error)}`);
1139
- } else if (result.checks.dependencies.count !== undefined) {
1140
- lines.push(` ${theme.muted(`${result.checks.dependencies.count} dependencies installed`)}`);
1141
- }
1142
- const tsconfigIcon = result.checks.configFiles.tsconfig ? theme.success("[PASS]") : theme.warning("[WARN]");
1143
- lines.push(`${tsconfigIcon} tsconfig.json`);
1144
- const srcIcon = result.checks.directories.src ? theme.success("[PASS]") : theme.warning("[WARN]");
1145
- lines.push(`${srcIcon} src/ directory`);
1146
- lines.push("", "=".repeat(50));
1147
- const summaryColor = result.exitCode === 0 ? theme.success : theme.error;
1148
- lines.push(summaryColor(`${result.summary.passed}/${result.summary.total} checks passed`));
1149
- if (result.exitCode !== 0) {
1150
- lines.push("", theme.muted("Run 'outfitter doctor' after fixing issues to verify."));
1151
- }
1152
- await output3(lines);
1153
- }
1154
- function doctorCommand(program) {
1155
- program.command("doctor").description("Validate environment and dependencies").option("--json", "Output as JSON", false).action(async (_flags, command) => {
1156
- const resolvedFlags = command.optsWithGlobals();
1157
- const outputOptions = resolvedFlags.json ? { mode: "json" } : undefined;
1158
- if (resolvedFlags.json) {
1159
- process.env["OUTFITTER_JSON"] = "1";
1160
- }
1161
- const result = await runDoctor({ cwd: process.cwd() });
1162
- await printDoctorResults(result, outputOptions);
1163
- process.exit(result.exitCode);
1164
- });
1165
- }
1166
-
1167
- // src/commands/init.ts
1168
- import {
1169
- existsSync as existsSync4,
1170
- mkdirSync as mkdirSync3,
1171
- readdirSync as readdirSync2,
1172
- readFileSync as readFileSync4,
1173
- statSync as statSync2,
1174
- writeFileSync as writeFileSync3
1175
- } from "node:fs";
1176
- import { basename as basename2, dirname as dirname3, extname as extname2, join as join4, resolve as resolve4 } from "node:path";
1177
- import { fileURLToPath as fileURLToPath3 } from "node:url";
1178
- import { exitWithError as exitWithError2, output as output4 } from "@outfitter/cli/output";
1179
- import { Result as Result4 } from "@outfitter/contracts";
1180
- class InitError extends Error {
1181
- _tag = "InitError";
1182
- constructor(message) {
1183
- super(message);
1184
- this.name = "InitError";
1185
- }
1186
- }
1187
- var BINARY_EXTENSIONS2 = new Set([
1188
- ".png",
1189
- ".jpg",
1190
- ".jpeg",
1191
- ".gif",
1192
- ".ico",
1193
- ".webp",
1194
- ".bmp",
1195
- ".tiff",
1196
- ".svg",
1197
- ".woff",
1198
- ".woff2",
1199
- ".ttf",
1200
- ".otf",
1201
- ".eot",
1202
- ".mp3",
1203
- ".mp4",
1204
- ".wav",
1205
- ".ogg",
1206
- ".webm",
1207
- ".zip",
1208
- ".tar",
1209
- ".gz",
1210
- ".bz2",
1211
- ".7z",
1212
- ".pdf",
1213
- ".exe",
1214
- ".dll",
1215
- ".so",
1216
- ".dylib",
1217
- ".node",
1218
- ".wasm",
1219
- ".bin",
1220
- ".dat",
1221
- ".db",
1222
- ".sqlite",
1223
- ".sqlite3"
1224
- ]);
1225
- function isBinaryFile2(filename) {
1226
- const ext = extname2(filename).toLowerCase();
1227
- return BINARY_EXTENSIONS2.has(ext);
1228
- }
1229
- function getTemplatesDir2() {
1230
- let currentDir = dirname3(fileURLToPath3(import.meta.url));
1231
- for (let i = 0;i < 10; i++) {
1232
- const templatesPath = join4(currentDir, "templates");
1233
- if (existsSync4(templatesPath)) {
1234
- return templatesPath;
1235
- }
1236
- currentDir = dirname3(currentDir);
1237
- }
1238
- return join4(process.cwd(), "templates");
1239
- }
1240
- function validateTemplate(templateName) {
1241
- const templatesDir = getTemplatesDir2();
1242
- const templatePath = join4(templatesDir, templateName);
1243
- if (!existsSync4(templatePath)) {
1244
- return Result4.err(new InitError(`Template '${templateName}' not found. Available templates are in: ${templatesDir}`));
1245
- }
1246
- return Result4.ok(templatePath);
1247
- }
1248
- function replacePlaceholders2(content, values) {
1249
- return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
1250
- if (Object.hasOwn(values, key)) {
1251
- return values[key];
1252
- }
1253
- return match;
1254
- });
1255
- }
1256
- function getOutputFilename2(templateFilename) {
1257
- if (templateFilename.endsWith(".template")) {
1258
- return templateFilename.slice(0, -".template".length);
1259
- }
1260
- return templateFilename;
1261
- }
1262
- function hasPackageJson(targetDir) {
1263
- return existsSync4(join4(targetDir, "package.json"));
1264
- }
1265
- function deriveProjectName3(packageName) {
1266
- if (packageName.startsWith("@")) {
1267
- const parts = packageName.split("/");
1268
- if (parts.length > 1 && parts[1]) {
1269
- return parts[1];
1270
- }
1271
- }
1272
- return packageName;
1273
- }
1274
- function resolvePackageName(options, resolvedTargetDir) {
1275
- return options.name ?? basename2(resolvedTargetDir);
1276
- }
1277
- function resolveBinName(options, projectName) {
1278
- return options.bin ?? deriveProjectName3(projectName);
1279
- }
1280
- function resolveTemplateName(options) {
1281
- return options.template ?? "basic";
1282
- }
1283
- function resolveAuthor() {
1284
- const fromEnv = process.env["GIT_AUTHOR_NAME"] ?? process.env["GIT_COMMITTER_NAME"] ?? process.env["AUTHOR"] ?? process.env["USER"] ?? process.env["USERNAME"];
1285
- if (fromEnv) {
1286
- return fromEnv;
1287
- }
1288
- try {
1289
- const result = Bun.spawnSync(["git", "config", "--get", "user.name"], {
1290
- stdout: "pipe",
1291
- stderr: "ignore"
1292
- });
1293
- if (result.exitCode === 0) {
1294
- const value = result.stdout.toString().trim();
1295
- return value.length > 0 ? value : "";
1296
- }
1297
- } catch {}
1298
- return "";
1299
- }
1300
- function resolveYear2() {
1301
- return String(new Date().getFullYear());
1302
- }
1303
- function resolveBlocks(options) {
1304
- if (options.noTooling) {
1305
- return;
1306
- }
1307
- if (options.with) {
1308
- const blocks = options.with.split(",").map((b) => b.trim()).filter(Boolean);
1309
- return blocks.length > 0 ? blocks : undefined;
1310
- }
1311
- return ["scaffolding"];
1312
- }
1313
- function copyTemplateFiles2(templateDir, targetDir, values, force, allowOverwrite = false) {
1314
- try {
1315
- if (!existsSync4(targetDir)) {
1316
- mkdirSync3(targetDir, { recursive: true });
1317
- }
1318
- const entries = readdirSync2(templateDir);
1319
- for (const entry of entries) {
1320
- const sourcePath = join4(templateDir, entry);
1321
- const stat = statSync2(sourcePath);
1322
- if (stat.isDirectory()) {
1323
- const targetSubDir = join4(targetDir, entry);
1324
- const result = copyTemplateFiles2(sourcePath, targetSubDir, values, force, allowOverwrite);
1325
- if (result.isErr()) {
1326
- return result;
1327
- }
1328
- } else if (stat.isFile()) {
1329
- const outputFilename = getOutputFilename2(entry);
1330
- const targetPath = join4(targetDir, outputFilename);
1331
- if (existsSync4(targetPath) && !force && !allowOverwrite) {
1332
- return Result4.err(new InitError(`File '${targetPath}' already exists. Use --force to overwrite.`));
1333
- }
1334
- if (isBinaryFile2(outputFilename)) {
1335
- const buffer = readFileSync4(sourcePath);
1336
- writeFileSync3(targetPath, buffer);
1337
- } else {
1338
- const content = readFileSync4(sourcePath, "utf-8");
1339
- const processedContent = replacePlaceholders2(content, values);
1340
- writeFileSync3(targetPath, processedContent, "utf-8");
1341
- }
1342
- }
1343
- }
1344
- return Result4.ok(undefined);
1345
- } catch (error) {
1346
- const message = error instanceof Error ? error.message : "Unknown error";
1347
- return Result4.err(new InitError(`Failed to copy template files: ${message}`));
1348
- }
1349
- }
1350
- var DEPENDENCY_SECTIONS2 = [
1351
- "dependencies",
1352
- "devDependencies",
1353
- "peerDependencies",
1354
- "optionalDependencies"
1355
- ];
1356
- function rewriteLocalDependencies2(targetDir) {
1357
- const packageJsonPath = join4(targetDir, "package.json");
1358
- if (!existsSync4(packageJsonPath)) {
1359
- return Result4.ok(undefined);
1360
- }
1361
- try {
1362
- const content = readFileSync4(packageJsonPath, "utf-8");
1363
- const parsed = JSON.parse(content);
1364
- let updated = false;
1365
- for (const section of DEPENDENCY_SECTIONS2) {
1366
- const deps = parsed[section];
1367
- if (!deps || typeof deps !== "object" || Array.isArray(deps)) {
1368
- continue;
1369
- }
1370
- const entries = deps;
1371
- for (const [name, version] of Object.entries(entries)) {
1372
- if (typeof version === "string" && name.startsWith("@outfitter/") && version !== "workspace:*") {
1373
- entries[name] = "workspace:*";
1374
- updated = true;
1375
- }
1376
- }
1377
- }
1378
- if (updated) {
1379
- writeFileSync3(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
1380
- `, "utf-8");
1381
- }
1382
- return Result4.ok(undefined);
1383
- } catch (error) {
1384
- const message = error instanceof Error ? error.message : "Unknown error";
1385
- return Result4.err(new InitError(`Failed to update local dependencies: ${message}`));
1386
- }
1387
- }
1388
- function injectSharedConfig2(targetDir) {
1389
- const packageJsonPath = join4(targetDir, "package.json");
1390
- if (!existsSync4(packageJsonPath)) {
1391
- return Result4.ok(undefined);
1392
- }
1393
- try {
1394
- const content = readFileSync4(packageJsonPath, "utf-8");
1395
- const parsed = JSON.parse(content);
1396
- const existingDevDeps = parsed["devDependencies"] ?? {};
1397
- parsed["devDependencies"] = { ...SHARED_DEV_DEPS, ...existingDevDeps };
1398
- const existingScripts = parsed["scripts"] ?? {};
1399
- parsed["scripts"] = { ...SHARED_SCRIPTS, ...existingScripts };
1400
- writeFileSync3(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
1401
- `, "utf-8");
1402
- return Result4.ok(undefined);
1403
- } catch (error) {
1404
- const message = error instanceof Error ? error.message : "Unknown error";
1405
- return Result4.err(new InitError(`Failed to inject shared config: ${message}`));
1406
- }
1407
- }
1408
- async function runInit(options) {
1409
- const { targetDir, force } = options;
1410
- const resolvedTargetDir = resolve4(targetDir);
1411
- if (hasPackageJson(resolvedTargetDir) && !force) {
1412
- return Result4.err(new InitError(`Directory '${resolvedTargetDir}' already has a package.json. ` + `Use --force to overwrite, or use 'outfitter add' to add tooling to an existing project.`));
1413
- }
1414
- const templateName = resolveTemplateName(options);
1415
- const templateResult = validateTemplate(templateName);
1416
- if (templateResult.isErr()) {
1417
- return templateResult;
1418
- }
1419
- const templatePath = templateResult.value;
1420
- const packageName = resolvePackageName(options, resolvedTargetDir);
1421
- const projectName = deriveProjectName3(packageName);
1422
- const binName = resolveBinName(options, projectName);
1423
- const author = resolveAuthor();
1424
- const year = resolveYear2();
1425
- const values = {
1426
- name: projectName,
1427
- projectName,
1428
- packageName,
1429
- binName,
1430
- version: "0.1.0",
1431
- description: "A new project created with Outfitter",
1432
- author,
1433
- year
1434
- };
1435
- try {
1436
- if (!existsSync4(resolvedTargetDir)) {
1437
- mkdirSync3(resolvedTargetDir, { recursive: true });
1438
- }
1439
- } catch (error) {
1440
- const message = error instanceof Error ? error.message : "Unknown error";
1441
- return Result4.err(new InitError(`Failed to create target directory: ${message}`));
1442
- }
1443
- const templatesDir = getTemplatesDir2();
1444
- const basePath = join4(templatesDir, "_base");
1445
- if (existsSync4(basePath)) {
1446
- const baseResult = copyTemplateFiles2(basePath, resolvedTargetDir, values, force);
1447
- if (baseResult.isErr()) {
1448
- return baseResult;
1449
- }
1450
- }
1451
- const copyResult = copyTemplateFiles2(templatePath, resolvedTargetDir, values, force, true);
1452
- if (copyResult.isErr()) {
1453
- return copyResult;
1454
- }
1455
- const injectResult = injectSharedConfig2(resolvedTargetDir);
1456
- if (injectResult.isErr()) {
1457
- return injectResult;
1458
- }
1459
- if (options.local) {
1460
- const rewriteResult = rewriteLocalDependencies2(resolvedTargetDir);
1461
- if (rewriteResult.isErr()) {
1462
- return rewriteResult;
1463
- }
1464
- }
1465
- const blocks = resolveBlocks(options);
1466
- let blocksAdded;
1467
- if (blocks && blocks.length > 0) {
1468
- const mergedResult = {
1469
- created: [],
1470
- skipped: [],
1471
- overwritten: [],
1472
- dependencies: {},
1473
- devDependencies: {}
1474
- };
1475
- for (const blockName of blocks) {
1476
- const addResult = await runAdd({
1477
- block: blockName,
1478
- force,
1479
- dryRun: false,
1480
- cwd: resolvedTargetDir
1481
- });
1482
- if (addResult.isErr()) {
1483
- return Result4.err(new InitError(`Failed to add block '${blockName}': ${addResult.error.message}`));
1484
- }
1485
- const blockResult = addResult.value;
1486
- mergedResult.created.push(...blockResult.created);
1487
- mergedResult.skipped.push(...blockResult.skipped);
1488
- mergedResult.overwritten.push(...blockResult.overwritten);
1489
- Object.assign(mergedResult.dependencies, blockResult.dependencies);
1490
- Object.assign(mergedResult.devDependencies, blockResult.devDependencies);
1491
- }
1492
- blocksAdded = mergedResult;
1493
- }
1494
- return Result4.ok({ blocksAdded });
1495
- }
1496
- async function printInitResults(targetDir, result, options) {
1497
- const mode = options?.mode;
1498
- if (mode === "json" || mode === "jsonl") {
1499
- await output4({
1500
- targetDir: resolve4(targetDir),
1501
- blocksAdded: result.blocksAdded ?? null,
1502
- nextSteps: ["bun install", "bun run dev"]
1503
- }, { mode });
1504
- return;
1505
- }
1506
- const lines = [
1507
- `Project initialized successfully in ${resolve4(targetDir)}`
1508
- ];
1509
- if (result.blocksAdded) {
1510
- const { created, skipped, dependencies, devDependencies } = result.blocksAdded;
1511
- if (created.length > 0) {
1512
- lines.push("", `Added ${created.length} tooling file(s):`);
1513
- for (const file of created) {
1514
- lines.push(` ✓ ${file}`);
1515
- }
1516
- }
1517
- if (skipped.length > 0) {
1518
- lines.push("", `Skipped ${skipped.length} existing file(s):`);
1519
- for (const file of skipped) {
1520
- lines.push(` - ${file}`);
1521
- }
1522
- }
1523
- const depCount = Object.keys(dependencies).length + Object.keys(devDependencies).length;
1524
- if (depCount > 0) {
1525
- lines.push("", `Added ${depCount} package(s) to package.json:`);
1526
- for (const [name, version] of Object.entries(dependencies)) {
1527
- lines.push(` + ${name}@${version}`);
1528
- }
1529
- for (const [name, version] of Object.entries(devDependencies)) {
1530
- lines.push(` + ${name}@${version} (dev)`);
1531
- }
1532
- }
1533
- }
1534
- lines.push("", "Next steps:", " bun install", " bun run dev");
1535
- await output4(lines);
1536
- }
1537
- function initCommand(program) {
1538
- const init = program.command("init").description("Scaffold a new Outfitter project");
1539
- const resolveFlags = (flags, command) => {
1540
- if (command) {
1541
- return command.optsWithGlobals();
1542
- }
1543
- return typeof flags.opts === "function" ? flags.opts() : flags;
1544
- };
1545
- const resolveLocal = (flags) => Boolean(flags.local || flags.workspace);
1546
- const withCommonOptions = (command) => command.option("-n, --name <name>", "Package name (defaults to directory name)").option("-b, --bin <name>", "Binary name (defaults to project name)").option("-f, --force", "Overwrite existing files", false).option("--local", "Use workspace:* for @outfitter dependencies", false).option("--workspace", "Alias for --local", false).option("--with <blocks>", "Tooling to add (comma-separated: scaffolding, claude, biome, lefthook, bootstrap)").option("--no-tooling", "Skip tooling setup").option("--json", "Output as JSON", false);
1547
- const resolveOutputMode = (flags) => {
1548
- if (flags.json) {
1549
- process.env["OUTFITTER_JSON"] = "1";
1550
- return "json";
1551
- }
1552
- return;
1553
- };
1554
- withCommonOptions(init.argument("[directory]").option("-t, --template <template>", "Template to use")).action(async (directory, flags, command) => {
1555
- const targetDir = directory ?? process.cwd();
1556
- const resolvedFlags = resolveFlags(flags, command);
1557
- const mode = resolveOutputMode(resolvedFlags);
1558
- const outputOptions = mode ? { mode } : undefined;
1559
- const local = resolveLocal(resolvedFlags);
1560
- const result = await runInit({
1561
- targetDir,
1562
- name: resolvedFlags.name,
1563
- template: resolvedFlags.template,
1564
- local,
1565
- force: resolvedFlags.force ?? false,
1566
- with: resolvedFlags.with,
1567
- noTooling: resolvedFlags.noTooling,
1568
- ...resolvedFlags.bin !== undefined ? { bin: resolvedFlags.bin } : {}
1569
- });
1570
- if (result.isErr()) {
1571
- exitWithError2(result.error, outputOptions);
1572
- }
1573
- await printInitResults(targetDir, result.value, outputOptions);
1574
- });
1575
- withCommonOptions(init.command("cli [directory]").description("Scaffold a new CLI project")).action(async (directory, flags, command) => {
1576
- const targetDir = directory ?? process.cwd();
1577
- const resolvedFlags = resolveFlags(flags, command);
1578
- const mode = resolveOutputMode(resolvedFlags);
1579
- const outputOptions = mode ? { mode } : undefined;
1580
- const local = resolveLocal(resolvedFlags);
1581
- const result = await runInit({
1582
- targetDir,
1583
- name: resolvedFlags.name,
1584
- template: "cli",
1585
- local,
1586
- force: resolvedFlags.force ?? false,
1587
- with: resolvedFlags.with,
1588
- noTooling: resolvedFlags.noTooling,
1589
- ...resolvedFlags.bin !== undefined ? { bin: resolvedFlags.bin } : {}
1590
- });
1591
- if (result.isErr()) {
1592
- exitWithError2(result.error, outputOptions);
1593
- }
1594
- await printInitResults(targetDir, result.value, outputOptions);
1595
- });
1596
- withCommonOptions(init.command("mcp [directory]").description("Scaffold a new MCP server")).action(async (directory, flags, command) => {
1597
- const targetDir = directory ?? process.cwd();
1598
- const resolvedFlags = resolveFlags(flags, command);
1599
- const mode = resolveOutputMode(resolvedFlags);
1600
- const outputOptions = mode ? { mode } : undefined;
1601
- const local = resolveLocal(resolvedFlags);
1602
- const result = await runInit({
1603
- targetDir,
1604
- name: resolvedFlags.name,
1605
- template: "mcp",
1606
- local,
1607
- force: resolvedFlags.force ?? false,
1608
- with: resolvedFlags.with,
1609
- noTooling: resolvedFlags.noTooling,
1610
- ...resolvedFlags.bin !== undefined ? { bin: resolvedFlags.bin } : {}
1611
- });
1612
- if (result.isErr()) {
1613
- exitWithError2(result.error, outputOptions);
1614
- }
1615
- await printInitResults(targetDir, result.value, outputOptions);
1616
- });
1617
- withCommonOptions(init.command("daemon [directory]").description("Scaffold a new daemon project")).action(async (directory, flags, command) => {
1618
- const targetDir = directory ?? process.cwd();
1619
- const resolvedFlags = resolveFlags(flags, command);
1620
- const mode = resolveOutputMode(resolvedFlags);
1621
- const outputOptions = mode ? { mode } : undefined;
1622
- const local = resolveLocal(resolvedFlags);
1623
- const result = await runInit({
1624
- targetDir,
1625
- name: resolvedFlags.name,
1626
- template: "daemon",
1627
- local,
1628
- force: resolvedFlags.force ?? false,
1629
- with: resolvedFlags.with,
1630
- noTooling: resolvedFlags.noTooling,
1631
- ...resolvedFlags.bin !== undefined ? { bin: resolvedFlags.bin } : {}
1632
- });
1633
- if (result.isErr()) {
1634
- exitWithError2(result.error, outputOptions);
1635
- }
1636
- await printInitResults(targetDir, result.value, outputOptions);
1637
- });
1638
- }
1639
-
1640
- // src/commands/migrate-kit.ts
1641
- import {
1642
- existsSync as existsSync5,
1643
- readdirSync as readdirSync3,
1644
- readFileSync as readFileSync5,
1645
- statSync as statSync3,
1646
- writeFileSync as writeFileSync4
1647
- } from "node:fs";
1648
- import { basename as basename3, dirname as dirname4, join as join5, relative, resolve as resolve5 } from "node:path";
1649
- import { output as output5 } from "@outfitter/cli/output";
1650
- import { Result as Result5 } from "@outfitter/contracts";
1651
- import ts from "typescript";
1652
- var FOUNDATION_IMPORT_MAP = {
1653
- "@outfitter/contracts": "@outfitter/kit/foundation/contracts",
1654
- "@outfitter/types": "@outfitter/kit/foundation/types"
1655
- };
1656
- var FOUNDATION_PACKAGES = [
1657
- "@outfitter/contracts",
1658
- "@outfitter/types"
1659
- ];
1660
- var DEPENDENCY_SECTIONS3 = [
1661
- "dependencies",
1662
- "devDependencies",
1663
- "peerDependencies",
1664
- "optionalDependencies"
1665
- ];
1666
- var SOURCE_EXTENSIONS = new Set([
1667
- ".ts",
1668
- ".tsx",
1669
- ".mts",
1670
- ".cts",
1671
- ".js",
1672
- ".jsx",
1673
- ".mjs",
1674
- ".cjs"
1675
- ]);
1676
- var IGNORED_DIRECTORIES = new Set([
1677
- ".git",
1678
- ".next",
1679
- ".turbo",
1680
- "build",
1681
- "coverage",
1682
- "dist",
1683
- "node_modules",
1684
- "out"
1685
- ]);
1686
-
1687
- class MigrateKitError extends Error {
1688
- _tag = "MigrateKitError";
1689
- constructor(message) {
1690
- super(message);
1691
- this.name = "MigrateKitError";
1692
- }
1693
- }
1694
- function normalizePath(filePath) {
1695
- return filePath.replaceAll("\\", "/");
1696
- }
1697
- function parseManifest(filePath) {
1698
- try {
1699
- const raw = readFileSync5(filePath, "utf-8");
1700
- const parsed = JSON.parse(raw);
1701
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1702
- return Result5.err(new MigrateKitError(`Invalid package.json at ${filePath}`));
1703
- }
1704
- return Result5.ok(parsed);
1705
- } catch (error) {
1706
- const message = error instanceof Error ? error.message : "Unknown error";
1707
- return Result5.err(new MigrateKitError(`Failed to read package.json at ${filePath}: ${message}`));
1708
- }
1709
- }
1710
- function resolveWorkspacePatterns(manifest) {
1711
- const workspaces = manifest["workspaces"];
1712
- if (Array.isArray(workspaces)) {
1713
- return workspaces.filter((value) => typeof value === "string");
1714
- }
1715
- if (!workspaces || typeof workspaces !== "object" || Array.isArray(workspaces)) {
1716
- return [];
1717
- }
1718
- const packages = workspaces.packages;
1719
- if (!Array.isArray(packages)) {
1720
- return [];
1721
- }
1722
- return packages.filter((value) => typeof value === "string");
1723
- }
1724
- function normalizeWorkspacePattern(pattern) {
1725
- let value = pattern.trim().replaceAll("\\", "/");
1726
- if (value.length === 0)
1727
- return value;
1728
- if (value.endsWith("/")) {
1729
- value = value.slice(0, -1);
1730
- }
1731
- if (value.endsWith("package.json")) {
1732
- return value;
1733
- }
1734
- return `${value}/package.json`;
1735
- }
1736
- function collectPackageJsonFiles(rootDir) {
1737
- const rootPackageJson = join5(rootDir, "package.json");
1738
- if (!existsSync5(rootPackageJson)) {
1739
- return Result5.err(new MigrateKitError(`No package.json found at ${rootPackageJson}`));
1740
- }
1741
- const manifestResult = parseManifest(rootPackageJson);
1742
- if (manifestResult.isErr()) {
1743
- return manifestResult;
1744
- }
1745
- const workspacePatterns = resolveWorkspacePatterns(manifestResult.value);
1746
- const files = new Set([rootPackageJson]);
1747
- for (const pattern of workspacePatterns) {
1748
- const normalized = normalizeWorkspacePattern(pattern);
1749
- if (normalized.length === 0) {
1750
- continue;
1751
- }
1752
- const glob = new Bun.Glob(normalized);
1753
- for (const entry of glob.scanSync({ cwd: rootDir })) {
1754
- const absolute = resolve5(rootDir, entry);
1755
- if (existsSync5(absolute) && basename3(absolute) === "package.json") {
1756
- files.add(absolute);
1757
- }
1758
- }
1759
- }
1760
- return Result5.ok(Array.from(files).sort((a, b) => a.localeCompare(b)));
1761
- }
1762
- function isSourceFile(filename) {
1763
- for (const extension of SOURCE_EXTENSIONS) {
1764
- if (filename.endsWith(extension)) {
1765
- return true;
1766
- }
1767
- }
1768
- return false;
1769
- }
1770
- function collectSourceFiles(packageDir) {
1771
- const files = [];
1772
- const stack = [packageDir];
1773
- while (stack.length > 0) {
1774
- const currentDir = stack.pop();
1775
- if (!currentDir)
1776
- continue;
1777
- let entries;
1778
- try {
1779
- entries = readdirSync3(currentDir).sort((a, b) => a.localeCompare(b));
1780
- } catch (error) {
1781
- const message = error instanceof Error ? error.message : "Unknown error";
1782
- return Result5.err(new MigrateKitError(`Failed to read source directory ${currentDir}: ${message}`));
1783
- }
1784
- for (const entry of entries) {
1785
- const fullPath = join5(currentDir, entry);
1786
- let stat;
1787
- try {
1788
- stat = statSync3(fullPath);
1789
- } catch (error) {
1790
- const message = error instanceof Error ? error.message : "Unknown error";
1791
- return Result5.err(new MigrateKitError(`Failed to read source tree entry ${fullPath}: ${message}`));
1792
- }
1793
- if (stat.isDirectory()) {
1794
- if (IGNORED_DIRECTORIES.has(entry)) {
1795
- continue;
1796
- }
1797
- if (existsSync5(join5(fullPath, "package.json"))) {
1798
- continue;
1799
- }
1800
- stack.push(fullPath);
1801
- continue;
1802
- }
1803
- if (!stat.isFile()) {
1804
- continue;
1805
- }
1806
- if (!isSourceFile(entry)) {
1807
- continue;
1808
- }
1809
- files.push(fullPath);
1810
- }
1811
- }
1812
- return Result5.ok(files.sort((a, b) => a.localeCompare(b)));
1813
- }
1814
- function getScriptKind(filePath) {
1815
- if (filePath.endsWith(".tsx"))
1816
- return ts.ScriptKind.TSX;
1817
- if (filePath.endsWith(".ts"))
1818
- return ts.ScriptKind.TS;
1819
- if (filePath.endsWith(".mts"))
1820
- return ts.ScriptKind.TS;
1821
- if (filePath.endsWith(".cts"))
1822
- return ts.ScriptKind.TS;
1823
- if (filePath.endsWith(".jsx"))
1824
- return ts.ScriptKind.JSX;
1825
- if (filePath.endsWith(".js"))
1826
- return ts.ScriptKind.JS;
1827
- if (filePath.endsWith(".mjs"))
1828
- return ts.ScriptKind.JS;
1829
- if (filePath.endsWith(".cjs"))
1830
- return ts.ScriptKind.JS;
1831
- return ts.ScriptKind.Unknown;
1832
- }
1833
- function mapFoundationSpecifier(specifier) {
1834
- return FOUNDATION_IMPORT_MAP[specifier];
1835
- }
1836
- function escapeRegex(value) {
1837
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1838
- }
1839
- function collectRemainingFoundationImports(content) {
1840
- const remaining = new Set;
1841
- for (const foundationPackage of FOUNDATION_PACKAGES) {
1842
- const pattern = new RegExp(`["']${escapeRegex(foundationPackage)}(?:\\/[^"'\\s]+)?["']`);
1843
- if (pattern.test(content)) {
1844
- remaining.add(foundationPackage);
1845
- }
1846
- }
1847
- return remaining;
1848
- }
1849
- function mergeFoundationImportsForPackage(byPackage, packageDir, imports) {
1850
- if (imports.size === 0) {
1851
- return;
1852
- }
1853
- const existing = byPackage.get(packageDir);
1854
- if (existing) {
1855
- for (const importPath of imports) {
1856
- existing.add(importPath);
1857
- }
1858
- return;
1859
- }
1860
- byPackage.set(packageDir, new Set(imports));
1861
- }
1862
- function findOwningPackageDir(filePath, packageDirs) {
1863
- const normalizedFilePath = normalizePath(filePath);
1864
- for (const packageDir of packageDirs) {
1865
- const normalizedPackageDir = normalizePath(packageDir);
1866
- if (normalizedFilePath === normalizedPackageDir || normalizedFilePath.startsWith(`${normalizedPackageDir}/`)) {
1867
- return packageDir;
1868
- }
1869
- }
1870
- return;
1871
- }
1872
- function quoteForLiteral(literalText) {
1873
- return literalText.startsWith("'") ? "'" : '"';
1874
- }
1875
- function enqueueModuleSpecifierReplacement(sourceFile, literal, replacements) {
1876
- const mapped = mapFoundationSpecifier(literal.text);
1877
- if (!mapped) {
1878
- return false;
1879
- }
1880
- const quote = quoteForLiteral(literal.getText(sourceFile));
1881
- replacements.push({
1882
- start: literal.getStart(sourceFile),
1883
- end: literal.getEnd(),
1884
- text: `${quote}${mapped}${quote}`
1885
- });
1886
- return true;
1887
- }
1888
- function enqueueImportTypeReplacement(sourceFile, node, replacements) {
1889
- const argument = node.argument;
1890
- if (!ts.isLiteralTypeNode(argument)) {
1891
- return false;
1892
- }
1893
- if (!ts.isStringLiteral(argument.literal)) {
1894
- return false;
1895
- }
1896
- return enqueueModuleSpecifierReplacement(sourceFile, argument.literal, replacements);
1897
- }
1898
- function enqueueCallExpressionReplacement(sourceFile, node, replacements) {
1899
- const [firstArg] = node.arguments;
1900
- if (!(firstArg && ts.isStringLiteral(firstArg))) {
1901
- return false;
1902
- }
1903
- const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
1904
- const isRequireCall = ts.isIdentifier(node.expression) && node.expression.text === "require";
1905
- if (!(isDynamicImport || isRequireCall)) {
1906
- return false;
1907
- }
1908
- return enqueueModuleSpecifierReplacement(sourceFile, firstArg, replacements);
1909
- }
1910
- function applyReplacements(content, replacements) {
1911
- if (replacements.length === 0) {
1912
- return content;
1913
- }
1914
- let next = content;
1915
- const ordered = [...replacements].sort((a, b) => b.start - a.start);
1916
- for (const replacement of ordered) {
1917
- next = next.slice(0, replacement.start) + replacement.text + next.slice(replacement.end);
1918
- }
1919
- return next;
1920
- }
1921
- function rewriteFoundationImports(filePath) {
1922
- let content;
1923
- try {
1924
- content = readFileSync5(filePath, "utf-8");
1925
- } catch (error) {
1926
- const message = error instanceof Error ? error.message : "Unknown error";
1927
- return Result5.err(new MigrateKitError(`Failed to read source file ${filePath}: ${message}`));
1928
- }
1929
- const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
1930
- const replacements = [];
1931
- let rewrites = 0;
1932
- function visit(node) {
1933
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
1934
- if (enqueueModuleSpecifierReplacement(sourceFile, node.moduleSpecifier, replacements)) {
1935
- rewrites += 1;
1936
- }
1937
- } else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
1938
- if (enqueueModuleSpecifierReplacement(sourceFile, node.moduleSpecifier, replacements)) {
1939
- rewrites += 1;
1940
- }
1941
- } else if (ts.isImportTypeNode(node)) {
1942
- if (enqueueImportTypeReplacement(sourceFile, node, replacements)) {
1943
- rewrites += 1;
1944
- }
1945
- } else if (ts.isCallExpression(node) && enqueueCallExpressionReplacement(sourceFile, node, replacements)) {
1946
- rewrites += 1;
1947
- }
1948
- ts.forEachChild(node, visit);
1949
- }
1950
- visit(sourceFile);
1951
- if (replacements.length === 0) {
1952
- return Result5.ok({ content, rewrites: 0 });
1953
- }
1954
- return Result5.ok({
1955
- content: applyReplacements(content, replacements),
1956
- rewrites
1957
- });
1958
- }
1959
- function sortRecord(value) {
1960
- if (!value)
1961
- return value;
1962
- return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
1963
- }
1964
- function rewriteManifestDependencies(content, options) {
1965
- let manifest;
1966
- try {
1967
- const parsed = JSON.parse(content);
1968
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1969
- return Result5.err(new MigrateKitError("package.json must contain an object"));
1970
- }
1971
- manifest = parsed;
1972
- } catch (error) {
1973
- const message = error instanceof Error ? error.message : "Unknown error";
1974
- return Result5.err(new MigrateKitError(`Invalid package.json JSON: ${message}`));
1975
- }
1976
- const keepFoundationPackages = options?.keepFoundationPackages;
1977
- const addKitDependency = options?.addKitDependency ?? false;
1978
- let changed = false;
1979
- let kitVersionCandidate;
1980
- let kitSectionCandidate;
1981
- for (const section of DEPENDENCY_SECTIONS3) {
1982
- const current = manifest[section];
1983
- if (!current || typeof current !== "object" || Array.isArray(current)) {
1984
- continue;
1985
- }
1986
- const deps = { ...current };
1987
- for (const foundationName of FOUNDATION_PACKAGES) {
1988
- const version = deps[foundationName];
1989
- if (typeof version !== "string") {
1990
- continue;
1991
- }
1992
- if (!kitVersionCandidate) {
1993
- kitVersionCandidate = version;
1994
- kitSectionCandidate = section;
1995
- }
1996
- if (keepFoundationPackages?.has(foundationName)) {
1997
- continue;
1998
- }
1999
- delete deps[foundationName];
2000
- changed = true;
2001
- }
2002
- manifest[section] = sortRecord(deps);
2003
- }
2004
- let hasKitDependency = false;
2005
- for (const section of DEPENDENCY_SECTIONS3) {
2006
- const current = manifest[section];
2007
- if (!current || typeof current !== "object" || Array.isArray(current)) {
2008
- continue;
2009
- }
2010
- const deps = current;
2011
- if (typeof deps["@outfitter/kit"] === "string") {
2012
- hasKitDependency = true;
2013
- break;
2014
- }
2015
- }
2016
- const shouldAddKit = addKitDependency || changed;
2017
- if (kitVersionCandidate && shouldAddKit && !hasKitDependency) {
2018
- const targetSection = kitSectionCandidate ?? "dependencies";
2019
- const existingSection = manifest[targetSection];
2020
- const deps = existingSection && typeof existingSection === "object" && !Array.isArray(existingSection) ? { ...existingSection } : {};
2021
- deps["@outfitter/kit"] = kitVersionCandidate;
2022
- manifest[targetSection] = sortRecord(deps);
2023
- changed = true;
2024
- }
2025
- if (!changed) {
2026
- return Result5.ok({ content, changed: false });
2027
- }
2028
- return Result5.ok({
2029
- content: `${JSON.stringify(manifest, null, 2)}
2030
- `,
2031
- changed: true
2032
- });
2033
- }
2034
- function splitLines(value) {
2035
- return value.replace(/\r\n/g, `
2036
- `).split(`
2037
- `);
2038
- }
2039
- function createLineDiff(before, after) {
2040
- const left = splitLines(before);
2041
- const right = splitLines(after);
2042
- const rows = left.length + 1;
2043
- const cols = right.length + 1;
2044
- const lcs = Array.from({ length: rows }, () => new Array(cols).fill(0));
2045
- for (let i2 = left.length - 1;i2 >= 0; i2 -= 1) {
2046
- const row = lcs[i2];
2047
- if (!row) {
2048
- continue;
2049
- }
2050
- for (let j2 = right.length - 1;j2 >= 0; j2 -= 1) {
2051
- const leftLine = left[i2];
2052
- const rightLine = right[j2];
2053
- if (leftLine === rightLine) {
2054
- row[j2] = 1 + (lcs[i2 + 1]?.[j2 + 1] ?? 0);
2055
- } else {
2056
- row[j2] = Math.max(lcs[i2 + 1]?.[j2] ?? 0, lcs[i2]?.[j2 + 1] ?? 0);
2057
- }
2058
- }
2059
- }
2060
- const ops = [];
2061
- let i = 0;
2062
- let j = 0;
2063
- while (i < left.length && j < right.length) {
2064
- const leftLine = left[i];
2065
- const rightLine = right[j];
2066
- if (leftLine === undefined || rightLine === undefined) {
2067
- break;
2068
- }
2069
- if (leftLine === rightLine) {
2070
- ops.push({ type: "equal", line: leftLine });
2071
- i += 1;
2072
- j += 1;
2073
- continue;
2074
- }
2075
- if ((lcs[i + 1]?.[j] ?? 0) >= (lcs[i]?.[j + 1] ?? 0)) {
2076
- ops.push({ type: "remove", line: leftLine });
2077
- i += 1;
2078
- } else {
2079
- ops.push({ type: "add", line: rightLine });
2080
- j += 1;
2081
- }
2082
- }
2083
- while (i < left.length) {
2084
- const line = left[i];
2085
- if (line === undefined)
2086
- break;
2087
- ops.push({ type: "remove", line });
2088
- i += 1;
2089
- }
2090
- while (j < right.length) {
2091
- const line = right[j];
2092
- if (line === undefined)
2093
- break;
2094
- ops.push({ type: "add", line });
2095
- j += 1;
2096
- }
2097
- return ops;
2098
- }
2099
- function createUnifiedDiff(filePath, before, after) {
2100
- if (before === after) {
2101
- return "";
2102
- }
2103
- const ops = createLineDiff(before, after);
2104
- const context = 3;
2105
- const changedIndexes = ops.map((op, index) => op.type === "equal" ? -1 : index).filter((index) => index >= 0);
2106
- if (changedIndexes.length === 0) {
2107
- return "";
2108
- }
2109
- const firstChanged = changedIndexes[0];
2110
- if (firstChanged === undefined) {
2111
- return "";
2112
- }
2113
- const hunks = [];
2114
- let groupStart = firstChanged;
2115
- let previous = firstChanged;
2116
- for (let index = 1;index < changedIndexes.length; index += 1) {
2117
- const current = changedIndexes[index];
2118
- if (current === undefined) {
2119
- continue;
2120
- }
2121
- if (current - previous <= context * 2) {
2122
- previous = current;
2123
- continue;
2124
- }
2125
- hunks.push({
2126
- start: Math.max(0, groupStart - context),
2127
- end: Math.min(ops.length, previous + context + 1)
2128
- });
2129
- groupStart = current;
2130
- previous = current;
2131
- }
2132
- hunks.push({
2133
- start: Math.max(0, groupStart - context),
2134
- end: Math.min(ops.length, previous + context + 1)
2135
- });
2136
- const oldLineAt = [];
2137
- const newLineAt = [];
2138
- let oldLine = 1;
2139
- let newLine = 1;
2140
- for (const op of ops) {
2141
- oldLineAt.push(oldLine);
2142
- newLineAt.push(newLine);
2143
- if (op.type !== "add") {
2144
- oldLine += 1;
2145
- }
2146
- if (op.type !== "remove") {
2147
- newLine += 1;
2148
- }
2149
- }
2150
- const lines = [`--- a/${filePath}`, `+++ b/${filePath}`];
2151
- for (const hunk of hunks) {
2152
- const oldStart = oldLineAt[hunk.start] ?? 1;
2153
- const newStart = newLineAt[hunk.start] ?? 1;
2154
- let oldCount = 0;
2155
- let newCount = 0;
2156
- for (let index = hunk.start;index < hunk.end; index += 1) {
2157
- const op = ops[index];
2158
- if (!op) {
2159
- continue;
2160
- }
2161
- if (op.type !== "add") {
2162
- oldCount += 1;
2163
- }
2164
- if (op.type !== "remove") {
2165
- newCount += 1;
2166
- }
2167
- }
2168
- lines.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
2169
- for (let index = hunk.start;index < hunk.end; index += 1) {
2170
- const op = ops[index];
2171
- if (!op) {
2172
- continue;
2173
- }
2174
- if (op.type === "equal") {
2175
- lines.push(` ${op.line}`);
2176
- } else if (op.type === "remove") {
2177
- lines.push(`-${op.line}`);
2178
- } else {
2179
- lines.push(`+${op.line}`);
2180
- }
2181
- }
2182
- }
2183
- return `${lines.join(`
2184
- `)}
2185
- `;
2186
- }
2187
- function writeChanges(changes, dryRun) {
2188
- if (dryRun) {
2189
- return Result5.ok(undefined);
2190
- }
2191
- for (const change of changes) {
2192
- try {
2193
- writeFileSync4(change.path, change.after, "utf-8");
2194
- } catch (error) {
2195
- const message = error instanceof Error ? error.message : "Unknown error";
2196
- return Result5.err(new MigrateKitError(`Failed to write ${change.path}: ${message}`));
2197
- }
2198
- }
2199
- return Result5.ok(undefined);
2200
- }
2201
- async function runMigrateKit(options) {
2202
- const targetDir = resolve5(options.targetDir ?? process.cwd());
2203
- const dryRun = options.dryRun ?? false;
2204
- const packageJsonResult = collectPackageJsonFiles(targetDir);
2205
- if (packageJsonResult.isErr()) {
2206
- return packageJsonResult;
2207
- }
2208
- const packageJsonFiles = packageJsonResult.value;
2209
- const packageDirectories = Array.from(new Set(packageJsonFiles.map((path) => resolve5(dirname4(path))))).sort((left, right) => right.length - left.length);
2210
- const sourceSet = new Set;
2211
- for (const packageDir of packageDirectories) {
2212
- const sourceFilesResult = collectSourceFiles(packageDir);
2213
- if (sourceFilesResult.isErr()) {
2214
- return sourceFilesResult;
2215
- }
2216
- for (const file of sourceFilesResult.value) {
2217
- sourceSet.add(file);
2218
- }
2219
- }
2220
- const sourceFiles = Array.from(sourceSet).sort((a, b) => a.localeCompare(b));
2221
- const remainingFoundationImportsByPackage = new Map;
2222
- const rewritesByPackage = new Map;
2223
- const changes = [];
2224
- const diffs = [];
2225
- let importRewrites = 0;
2226
- let manifestUpdates = 0;
2227
- for (const sourceFile of sourceFiles) {
2228
- const rewriteResult = rewriteFoundationImports(sourceFile);
2229
- if (rewriteResult.isErr()) {
2230
- return rewriteResult;
2231
- }
2232
- const packageDir = findOwningPackageDir(sourceFile, packageDirectories);
2233
- if (packageDir) {
2234
- const remainingImports = collectRemainingFoundationImports(rewriteResult.value.content);
2235
- mergeFoundationImportsForPackage(remainingFoundationImportsByPackage, packageDir, remainingImports);
2236
- if (rewriteResult.value.rewrites > 0) {
2237
- rewritesByPackage.set(packageDir, (rewritesByPackage.get(packageDir) ?? 0) + rewriteResult.value.rewrites);
2238
- }
2239
- }
2240
- if (rewriteResult.value.rewrites === 0) {
2241
- continue;
2242
- }
2243
- const before = readFileSync5(sourceFile, "utf-8");
2244
- const after = rewriteResult.value.content;
2245
- if (before === after) {
2246
- continue;
2247
- }
2248
- importRewrites += rewriteResult.value.rewrites;
2249
- const path = normalizePath(relative(targetDir, sourceFile));
2250
- changes.push({ path: sourceFile, before, after });
2251
- diffs.push({ path, preview: createUnifiedDiff(path, before, after) });
2252
- }
2253
- for (const packageJsonPath of packageJsonFiles) {
2254
- let before;
2255
- try {
2256
- before = readFileSync5(packageJsonPath, "utf-8");
2257
- } catch (error) {
2258
- const message = error instanceof Error ? error.message : "Unknown error";
2259
- return Result5.err(new MigrateKitError(`Failed to read ${packageJsonPath}: ${message}`));
2260
- }
2261
- const packageDir = resolve5(dirname4(packageJsonPath));
2262
- const keepFoundationPackages = remainingFoundationImportsByPackage.get(packageDir);
2263
- const rewriteResult = rewriteManifestDependencies(before, {
2264
- ...keepFoundationPackages ? { keepFoundationPackages } : {},
2265
- addKitDependency: (rewritesByPackage.get(packageDir) ?? 0) > 0
2266
- });
2267
- if (rewriteResult.isErr()) {
2268
- return rewriteResult;
2269
- }
2270
- if (!rewriteResult.value.changed || rewriteResult.value.content === before) {
2271
- continue;
2272
- }
2273
- manifestUpdates += 1;
2274
- const path = normalizePath(relative(targetDir, packageJsonPath));
2275
- changes.push({
2276
- path: packageJsonPath,
2277
- before,
2278
- after: rewriteResult.value.content
2279
- });
2280
- diffs.push({
2281
- path,
2282
- preview: createUnifiedDiff(path, before, rewriteResult.value.content)
2283
- });
2284
- }
2285
- const writeResult = writeChanges(changes, dryRun);
2286
- if (writeResult.isErr()) {
2287
- return writeResult;
2288
- }
2289
- const changedFiles = changes.map((change) => normalizePath(relative(targetDir, change.path))).sort((a, b) => a.localeCompare(b));
2290
- return Result5.ok({
2291
- targetDir,
2292
- dryRun,
2293
- packageJsonFiles: packageJsonFiles.length,
2294
- sourceFiles: sourceFiles.length,
2295
- manifestUpdates,
2296
- importRewrites,
2297
- changedFiles,
2298
- diffs: diffs.sort((left, right) => left.path.localeCompare(right.path))
2299
- });
2300
- }
2301
- async function printMigrateKitResults(result, options) {
2302
- const mode = options?.mode;
2303
- if (mode === "json" || mode === "jsonl") {
2304
- await output5(result, { mode });
2305
- return;
2306
- }
2307
- const lines = [];
2308
- if (result.changedFiles.length === 0) {
2309
- lines.push("No kit migration changes needed.");
2310
- await output5(lines);
2311
- return;
2312
- }
2313
- lines.push(result.dryRun ? "Dry run complete. No files were written." : "Migration complete.", "");
2314
- lines.push(`Changed files: ${result.changedFiles.length}`);
2315
- lines.push(`Import rewrites: ${result.importRewrites}`);
2316
- lines.push(`Manifest updates: ${result.manifestUpdates}`, "");
2317
- for (const file of result.changedFiles) {
2318
- lines.push(` - ${file}`);
2319
- }
2320
- if (result.dryRun && result.diffs.length > 0) {
2321
- lines.push("", "Diff preview:", "");
2322
- for (const diff of result.diffs) {
2323
- lines.push(diff.preview.trimEnd(), "");
2324
- }
2325
- }
2326
- await output5(lines);
2327
- }
2328
- function migrateKitCommand(program) {
2329
- const existingMigrate = program.commands.find((command) => command.name() === "migrate");
2330
- const migrate = existingMigrate ?? program.command("migrate").description("Migration commands");
2331
- if (migrate.commands.some((command) => command.name() === "kit")) {
2332
- return;
2333
- }
2334
- migrate.command("kit [directory]").description("Migrate foundation imports and dependencies to @outfitter/kit").option("--dry-run", "Preview changes without writing files", false).option("--json", "Output result as JSON", false).action(async (directory, flags) => {
2335
- if (flags.json) {
2336
- process.env["OUTFITTER_JSON"] = "1";
2337
- }
2338
- const result = await runMigrateKit({
2339
- ...directory ? { targetDir: directory } : {},
2340
- dryRun: Boolean(flags.dryRun)
2341
- });
2342
- if (result.isErr()) {
2343
- throw result.error;
2344
- }
2345
- await printMigrateKitResults(result.value, flags.json ? { mode: "json" } : undefined);
2346
- });
2347
- }
2348
-
2349
- // src/actions.ts
2350
- import { resolve as resolve7 } from "node:path";
2351
- import { output as output8 } from "@outfitter/cli/output";
2352
- import {
2353
- createActionRegistry,
2354
- defineAction,
2355
- InternalError as InternalError2,
2356
- Result as Result7
2357
- } from "@outfitter/contracts";
2358
- import { z } from "zod";
2359
-
2360
- // src/commands/demo.ts
2361
- import { isCancel as isCancel2, select as select2 } from "@clack/prompts";
2362
- import { output as output6 } from "@outfitter/cli/output";
2363
- import { createTheme as createTheme3, renderTable as renderTable2, SPINNERS } from "@outfitter/cli/render";
2364
- import { ANSI } from "@outfitter/cli/streaming";
2365
- import { isInteractive } from "@outfitter/cli/terminal";
2366
-
2367
- // src/commands/demo/index.ts
2368
- import {
2369
- getPrimitive,
2370
- getPrimitiveIds,
2371
- renderAllDemos,
2372
- renderDemo
2373
- } from "@outfitter/cli/demo";
2374
- var appSections = [];
2375
- var DEMO_CONFIG = {
2376
- examples: {
2377
- success: "Package published",
2378
- error: "Init failed",
2379
- warning: "Deprecated API",
2380
- spinnerMessage: "Installing dependencies...",
2381
- boxTitle: "Outfitter",
2382
- boxContent: "CLI scaffolding tool"
2383
- }
2384
- };
2385
- function registerSection(section) {
2386
- appSections.push(section);
2387
- }
2388
- function getSectionIds() {
2389
- const primitiveIds = getPrimitiveIds();
2390
- const appIds = appSections.map((s) => s.id);
2391
- return [...primitiveIds, ...appIds];
2392
- }
2393
- function getSections() {
2394
- const primitiveSections = getPrimitiveIds().map((id) => {
2395
- const meta = getPrimitive(id);
2396
- return {
2397
- id: meta.id,
2398
- description: meta.description,
2399
- run: () => renderDemo(id, DEMO_CONFIG)
2400
- };
2401
- });
2402
- return [...primitiveSections, ...appSections];
2403
- }
2404
- function getSection(id) {
2405
- const primitiveIds = getPrimitiveIds();
2406
- if (primitiveIds.includes(id)) {
2407
- const meta = getPrimitive(id);
2408
- return {
2409
- id: meta.id,
2410
- description: meta.description,
2411
- run: () => renderDemo(id, DEMO_CONFIG)
2412
- };
2413
- }
2414
- return appSections.find((s) => s.id === id);
2415
- }
2416
- function runSection(id) {
2417
- const section = getSection(id);
2418
- if (!section) {
2419
- return;
2420
- }
2421
- return section.run();
2422
- }
2423
- function runAllSections() {
2424
- const outputs = [];
2425
- outputs.push(renderAllDemos(DEMO_CONFIG));
2426
- for (const section of appSections) {
2427
- outputs.push("");
2428
- outputs.push(section.run());
2429
- }
2430
- return outputs.join(`
2431
- `).trimEnd();
2432
- }
2433
-
2434
- // src/commands/demo/errors.ts
2435
- import { createTheme as createTheme2, renderTable } from "@outfitter/cli/render";
2436
- import {
2437
- AuthError,
2438
- exitCodeMap,
2439
- NotFoundError,
2440
- statusCodeMap,
2441
- ValidationError as ValidationError2
2442
- } from "@outfitter/contracts";
2443
- function getErrorInfos() {
2444
- return [
2445
- { name: "ValidationError", category: "validation" },
2446
- { name: "NotFoundError", category: "not_found" },
2447
- { name: "ConflictError", category: "conflict" },
2448
- { name: "PermissionError", category: "permission" },
2449
- { name: "TimeoutError", category: "timeout" },
2450
- { name: "RateLimitError", category: "rate_limit" },
2451
- { name: "NetworkError", category: "network" },
2452
- { name: "InternalError", category: "internal" },
2453
- { name: "AuthError", category: "auth" },
2454
- { name: "CancelledError", category: "cancelled" }
2455
- ];
2456
- }
2457
- function formatErrorHuman(error) {
2458
- return `${error._tag}: ${error.message}`;
2459
- }
2460
- function formatErrorJson(error) {
2461
- const result = {
2462
- message: error.message,
2463
- _tag: error._tag,
2464
- category: error.category
2465
- };
2466
- if (error.context !== undefined) {
2467
- result["context"] = error.context;
2468
- }
2469
- return JSON.stringify(result);
2470
- }
2471
- function runErrorsDemo() {
2472
- const theme = createTheme2();
2473
- const lines = [];
2474
- lines.push("ERROR OUTPUT");
2475
- lines.push("============");
2476
- lines.push("");
2477
- lines.push('import { exitWithError } from "@outfitter/cli";');
2478
- lines.push('import { ValidationError, NotFoundError } from "@outfitter/contracts";');
2479
- lines.push("");
2480
- const validationError = new ValidationError2({
2481
- message: "Invalid email format",
2482
- field: "email"
2483
- });
2484
- lines.push("// Human mode (TTY):");
2485
- lines.push('exitWithError(new ValidationError({ message: "Invalid email format" }))');
2486
- lines.push(`→ stderr: "${formatErrorHuman(validationError)}"`);
2487
- lines.push(`→ exit code: ${validationError.exitCode()}`);
2488
- lines.push("");
2489
- const notFoundError = new NotFoundError({
2490
- message: "User not found",
2491
- resourceType: "user",
2492
- resourceId: "abc123"
2493
- });
2494
- lines.push('exitWithError(new NotFoundError({ message: "User not found", ... }))');
2495
- lines.push(`→ stderr: "${formatErrorHuman(notFoundError)}"`);
2496
- lines.push(`→ exit code: ${notFoundError.exitCode()}`);
2497
- lines.push("");
2498
- lines.push("// JSON mode (pipe/CI):");
2499
- const validationWithContext = new ValidationError2({
2500
- message: "Invalid email",
2501
- field: "email"
2502
- });
2503
- lines.push('exitWithError(new ValidationError({ message: "Invalid email", field: "email" }))');
2504
- lines.push(`→ stderr: ${formatErrorJson({
2505
- ...validationWithContext,
2506
- context: { field: "email" }
2507
- })}`);
2508
- lines.push(`→ exit code: ${validationWithContext.exitCode()}`);
2509
- lines.push("");
2510
- lines.push("ERROR TAXONOMY → EXIT CODES");
2511
- lines.push("===========================");
2512
- lines.push("");
2513
- const taxonomyData = getErrorInfos().map((info) => ({
2514
- Category: info.category,
2515
- "Exit Code": exitCodeMap[info.category],
2516
- "HTTP Status": statusCodeMap[info.category],
2517
- "Example Error": info.name
2518
- }));
2519
- lines.push(renderTable(taxonomyData));
2520
- lines.push("");
2521
- lines.push("CREATING ERRORS");
2522
- lines.push("===============");
2523
- lines.push("");
2524
- lines.push('import { ValidationError, NotFoundError, TimeoutError } from "@outfitter/contracts";');
2525
- lines.push("");
2526
- lines.push("// Input validation failed");
2527
- lines.push('new ValidationError({ message: "Email format invalid", field: "email" })');
2528
- lines.push("");
2529
- lines.push("// Resource not found");
2530
- lines.push('new NotFoundError({ message: "note not found: abc123", resourceType: "note", resourceId: "abc123" })');
2531
- lines.push("");
2532
- lines.push("// Operation timed out");
2533
- lines.push('new TimeoutError({ message: "Database query timed out", operation: "query", timeoutMs: 5000 })');
2534
- lines.push("");
2535
- lines.push("MODE DETECTION");
2536
- lines.push("==============");
2537
- lines.push("");
2538
- lines.push("Output mode auto-detected:");
2539
- lines.push("- TTY → human-readable format");
2540
- lines.push("- Pipe/CI → JSON format");
2541
- lines.push("- OUTFITTER_JSON=1 → force JSON");
2542
- lines.push("- OUTFITTER_JSONL=1 → force JSONL");
2543
- lines.push("");
2544
- lines.push("ERROR METHODS");
2545
- lines.push("=============");
2546
- lines.push("");
2547
- lines.push("All error classes provide:");
2548
- lines.push("- .exitCode() → CLI exit code (from exitCodeMap)");
2549
- lines.push("- .statusCode() → HTTP status code (from statusCodeMap)");
2550
- lines.push("- .category → error category string");
2551
- lines.push("- ._tag → error type name (e.g., 'ValidationError')");
2552
- lines.push("");
2553
- const authError = new AuthError({
2554
- message: "Invalid token",
2555
- reason: "expired"
2556
- });
2557
- lines.push(theme.muted("Example:"));
2558
- lines.push(`const err = new AuthError({ message: "Invalid token", reason: "expired" })`);
2559
- lines.push(`err.exitCode() // ${authError.exitCode()}`);
2560
- lines.push(`err.statusCode() // ${authError.statusCode()}`);
2561
- lines.push(`err.category // "${authError.category}"`);
2562
- lines.push(`err._tag // "${authError._tag}"`);
2563
- return lines.join(`
2564
- `);
2565
- }
2566
- registerSection({
2567
- id: "errors",
2568
- description: "Error taxonomy and output formatting",
2569
- run: runErrorsDemo
2570
- });
2571
-
2572
- // src/commands/demo.ts
2573
- async function runDemo(options) {
2574
- if (options.list) {
2575
- return listSections();
2576
- }
2577
- if (options.animate) {
2578
- await runAnimatedSpinnerDemo();
2579
- return { output: "", exitCode: 0 };
2580
- }
2581
- if (options.section) {
2582
- return runSectionByName(options.section);
2583
- }
2584
- if (isInteractive()) {
2585
- const selectedSection = await selectSection();
2586
- if (selectedSection === null) {
2587
- return { output: "", exitCode: 130 };
2588
- }
2589
- return runSectionByName(selectedSection);
2590
- }
2591
- const output7 = runAllSections();
2592
- return { output: output7, exitCode: 0 };
2593
- }
2594
- function runSectionByName(sectionName) {
2595
- if (sectionName === "all") {
2596
- const output8 = runAllSections();
2597
- return { output: output8, exitCode: 0 };
2598
- }
2599
- const output7 = runSection(sectionName);
2600
- if (output7 === undefined) {
2601
- return {
2602
- output: formatError(sectionName),
2603
- exitCode: 1
2604
- };
2605
- }
2606
- return { output: output7, exitCode: 0 };
2607
- }
2608
- async function selectSection() {
2609
- const sections = getSections();
2610
- const options = [
2611
- { value: "all", label: "All sections", hint: "Run all demos" },
2612
- ...sections.map((s) => ({
2613
- value: s.id,
2614
- label: s.id,
2615
- hint: s.description
2616
- }))
2617
- ];
2618
- const selection = await select2({
2619
- message: "Select a demo section",
2620
- options
2621
- });
2622
- if (isCancel2(selection)) {
2623
- return null;
2624
- }
2625
- return String(selection);
2626
- }
2627
- function listSections() {
2628
- const sections = getSections();
2629
- const theme = createTheme3();
2630
- if (sections.length === 0) {
2631
- return {
2632
- output: theme.muted("No demo sections available."),
2633
- exitCode: 0
2634
- };
2635
- }
2636
- const lines = [
2637
- "Available demo sections:",
2638
- "",
2639
- renderTable2(sections.map((s) => ({ section: s.id, description: s.description })), { headers: { section: "Section", description: "Description" } }),
2640
- "",
2641
- theme.muted('Run "outfitter demo [section]" to run a specific section.'),
2642
- theme.muted('Run "outfitter demo all" to run all sections.')
2643
- ];
2644
- return { output: lines.join(`
2645
- `), exitCode: 0 };
2646
- }
2647
- function formatError(sectionId) {
2648
- const theme = createTheme3();
2649
- const available = getSectionIds();
2650
- const lines = [
2651
- theme.error(`Unknown section: ${sectionId}`),
2652
- "",
2653
- `Available sections: ${available.length > 0 ? available.join(", ") : "(none)"}`,
2654
- "",
2655
- theme.muted('Run "outfitter demo --list" to see all sections.')
2656
- ];
2657
- return lines.join(`
2658
- `);
2659
- }
2660
- async function runAnimatedSpinnerDemo() {
2661
- const theme = createTheme3();
2662
- const styles = Object.keys(SPINNERS);
2663
- const intervalMs = 80;
2664
- const stream = process.stdout;
2665
- const isTTY = stream.isTTY ?? false;
2666
- const writeLine = (line) => {
2667
- stream.write(`${line}
2668
- `);
2669
- };
2670
- writeLine("");
2671
- writeLine(theme.bold("ANIMATED SPINNER DEMO"));
2672
- writeLine(theme.muted("All styles running simultaneously"));
2673
- writeLine(theme.muted("Press Ctrl+C to stop"));
2674
- writeLine("");
2675
- if (!isTTY) {
2676
- for (const style of styles) {
2677
- const spinner = SPINNERS[style];
2678
- writeLine(`${style.padEnd(10)} ${spinner.frames.join(" ")}`);
2679
- }
2680
- return;
2681
- }
2682
- stream.write(ANSI.hideCursor);
2683
- const numLines = styles.length;
2684
- for (const style of styles) {
2685
- const spinner = SPINNERS[style];
2686
- const frame = spinner.frames[0] ?? "";
2687
- stream.write(` ${frame} ${style.padEnd(10)} ${theme.muted(`(${spinner.interval}ms)`)}
2688
- `);
2689
- }
2690
- const frameIndices = styles.map(() => 0);
2691
- const animate = () => {
2692
- stream.write(ANSI.cursorUp(numLines));
2693
- for (let i = 0;i < styles.length; i++) {
2694
- const style = styles[i];
2695
- if (!style)
2696
- continue;
2697
- const spinner = SPINNERS[style];
2698
- const frameIndex = frameIndices[i] ?? 0;
2699
- const frame = spinner.frames[frameIndex % spinner.frames.length] ?? "";
2700
- stream.write(ANSI.clearLine);
2701
- stream.write(` ${frame} ${style.padEnd(10)} ${theme.muted(`(${spinner.interval}ms)`)}
2702
- `);
2703
- frameIndices[i] = frameIndex + 1;
2704
- }
2705
- };
2706
- const timer = setInterval(animate, intervalMs);
2707
- const cleanup = () => {
2708
- clearInterval(timer);
2709
- stream.write(ANSI.showCursor);
2710
- writeLine("");
2711
- writeLine(theme.muted("Use createSpinner() from @outfitter/cli/streaming"));
2712
- writeLine("");
2713
- process.exit(0);
2714
- };
2715
- process.on("SIGINT", cleanup);
2716
- await new Promise(() => {});
2717
- }
2718
- async function printDemoResults(result) {
2719
- if (result.output) {
2720
- await output6(result.output, { mode: "human" });
2721
- }
2722
- }
2723
-
2724
- // src/commands/update.ts
2725
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
2726
- import { join as join6, resolve as resolve6 } from "node:path";
2727
- import { output as output7 } from "@outfitter/cli/output";
2728
- import { createTheme as createTheme4 } from "@outfitter/cli/render";
2729
- import { InternalError, Result as Result6 } from "@outfitter/contracts";
2730
- function getInstalledPackages(cwd) {
2731
- const pkgPath = join6(cwd, "package.json");
2732
- if (!existsSync6(pkgPath)) {
2733
- return Result6.err(InternalError.create("No package.json found", { cwd }));
2734
- }
2735
- let raw;
2736
- try {
2737
- raw = readFileSync6(pkgPath, "utf-8");
2738
- } catch {
2739
- return Result6.err(InternalError.create("Failed to read package.json", { cwd }));
2740
- }
2741
- let pkg;
2742
- try {
2743
- pkg = JSON.parse(raw);
2744
- } catch {
2745
- return Result6.err(InternalError.create("Invalid JSON in package.json", { cwd }));
2746
- }
2747
- const deps = {
2748
- ...pkg.dependencies ?? {},
2749
- ...pkg.devDependencies ?? {}
2750
- };
2751
- const packages = [];
2752
- for (const [name, version] of Object.entries(deps)) {
2753
- if (!name.startsWith("@outfitter/"))
2754
- continue;
2755
- if (version.startsWith("workspace:")) {
2756
- const wsVersion = version.slice("workspace:".length);
2757
- if (wsVersion === "*" || wsVersion === "~" || wsVersion === "^") {
2758
- continue;
2759
- }
2760
- const wsClean = wsVersion.replace(/^[\^~>=<]+/, "");
2761
- try {
2762
- if (!Bun.semver.satisfies(wsClean, "*"))
2763
- continue;
2764
- } catch {
2765
- continue;
2766
- }
2767
- packages.push({ name, version: wsClean });
2768
- continue;
2769
- }
2770
- const cleaned = version.replace(/^[\^~>=<]+/, "");
2771
- try {
2772
- if (!Bun.semver.satisfies(cleaned, "*"))
2773
- continue;
2774
- } catch {
2775
- continue;
2776
- }
2777
- packages.push({ name, version: cleaned });
2778
- }
2779
- return Result6.ok(packages);
2780
- }
2781
- async function getLatestVersion(name) {
2782
- try {
2783
- const proc = Bun.spawn(["npm", "view", name, "version"], {
2784
- stdout: "pipe",
2785
- stderr: "pipe"
2786
- });
2787
- const stdout = await new Response(proc.stdout).text();
2788
- const exitCode = await proc.exited;
2789
- if (exitCode !== 0)
2790
- return null;
2791
- return stdout.trim() || null;
2792
- } catch {
2793
- return null;
2794
- }
2795
- }
2796
- var MIGRATION_DOC_PATHS = [
2797
- "plugins/kit/shared/migrations",
2798
- "node_modules/@outfitter/kit/shared/migrations"
2799
- ];
2800
- function findMigrationDocsDir(cwd, binaryDir) {
2801
- for (const relative2 of MIGRATION_DOC_PATHS) {
2802
- const dir = join6(cwd, relative2);
2803
- if (existsSync6(dir))
2804
- return dir;
2805
- }
2806
- let current = resolve6(cwd);
2807
- const root = resolve6("/");
2808
- while (current !== root) {
2809
- const parent = resolve6(current, "..");
2810
- if (parent === current)
2811
- break;
2812
- current = parent;
2813
- for (const relative2 of MIGRATION_DOC_PATHS) {
2814
- const dir = join6(current, relative2);
2815
- if (existsSync6(dir))
2816
- return dir;
2817
- }
2818
- }
2819
- const resolvedBinaryDir = binaryDir ?? resolve6(import.meta.dir, "../../../..");
2820
- for (const relative2 of MIGRATION_DOC_PATHS) {
2821
- const dir = join6(resolvedBinaryDir, relative2);
2822
- if (existsSync6(dir))
2823
- return dir;
2824
- }
2825
- return null;
2826
- }
2827
- function readMigrationDocs(migrationsDir, shortName, fromVersion, toVersion) {
2828
- const glob = new Bun.Glob(`outfitter-${shortName}-*.md`);
2829
- const versionPattern = new RegExp(`^outfitter-${shortName}-(\\d+\\.\\d+\\.\\d+)\\.md$`);
2830
- const docs = [];
2831
- for (const entry of glob.scanSync({ cwd: migrationsDir })) {
2832
- const match = entry.match(versionPattern);
2833
- if (!match?.[1])
2834
- continue;
2835
- const docVersion = match[1];
2836
- if (Bun.semver.order(docVersion, fromVersion) <= 0)
2837
- continue;
2838
- if (Bun.semver.order(docVersion, toVersion) > 0)
2839
- continue;
2840
- const filePath = join6(migrationsDir, entry);
2841
- const content = readFileSync6(filePath, "utf-8");
2842
- const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
2843
- if (body) {
2844
- docs.push({ version: docVersion, content: body });
2845
- }
2846
- }
2847
- docs.sort((a, b) => Bun.semver.order(a.version, b.version));
2848
- return docs.map((d) => d.content);
2849
- }
2850
- async function runUpdate(options) {
2851
- const cwd = resolve6(options.cwd);
2852
- const installedResult = getInstalledPackages(cwd);
2853
- if (installedResult.isErr())
2854
- return installedResult;
2855
- const installed = installedResult.value;
2856
- if (installed.length === 0) {
2857
- return Result6.ok({
2858
- packages: [],
2859
- total: 0,
2860
- updatesAvailable: 0,
2861
- hasBreaking: false
2862
- });
2863
- }
2864
- const packages = await Promise.all(installed.map(async (pkg) => {
2865
- const latest = await getLatestVersion(pkg.name);
2866
- const updateAvailable = latest !== null && Bun.semver.order(latest, pkg.version) > 0;
2867
- const breaking = updateAvailable && latest !== null ? getMajor(latest) > getMajor(pkg.version) : false;
2868
- return {
2869
- name: pkg.name,
2870
- current: pkg.version,
2871
- latest,
2872
- updateAvailable,
2873
- breaking
2874
- };
2875
- }));
2876
- const updatesAvailable = packages.filter((p) => p.updateAvailable).length;
2877
- const hasBreaking = packages.some((p) => p.breaking);
2878
- return Result6.ok({
2879
- packages,
2880
- total: packages.length,
2881
- updatesAvailable,
2882
- hasBreaking
2883
- });
2884
- }
2885
- function getMajor(version) {
2886
- const parts = version.split(".");
2887
- return Number.parseInt(parts[0] ?? "0", 10);
2888
- }
2889
- async function printUpdateResults(result, options) {
2890
- const mode = options?.mode;
2891
- if (mode === "json" || mode === "jsonl") {
2892
- await output7(result, { mode });
2893
- return;
2894
- }
2895
- const theme = createTheme4();
2896
- const lines = ["", "Outfitter Update", "", "=".repeat(60)];
2897
- if (result.packages.length === 0) {
2898
- lines.push("No @outfitter/* packages found in package.json.");
2899
- await output7(lines);
2900
- return;
2901
- }
2902
- lines.push(` ${"Package".padEnd(28)} ${"Current".padEnd(10)} ${"Available".padEnd(10)} Migration`);
2903
- lines.push(` ${"─".repeat(28)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(20)}`);
2904
- for (const pkg of result.packages) {
2905
- const name = pkg.name.padEnd(28);
2906
- const current = pkg.current.padEnd(10);
2907
- const available = (pkg.latest ?? "unknown").padEnd(10);
2908
- let migration;
2909
- if (pkg.latest === null) {
2910
- migration = theme.muted("lookup failed");
2911
- } else if (!pkg.updateAvailable) {
2912
- migration = theme.muted("up to date");
2913
- } else if (pkg.breaking) {
2914
- migration = theme.error("major (breaking)");
2915
- } else {
2916
- migration = theme.success("minor (no breaking)");
2917
- }
2918
- lines.push(` ${name} ${current} ${available} ${migration}`);
2919
- }
2920
- lines.push("");
2921
- if (result.updatesAvailable > 0) {
2922
- lines.push(theme.muted("Run 'outfitter update --guide' for migration instructions."));
2923
- } else {
2924
- lines.push(theme.success("All packages are up to date."));
2925
- }
2926
- if (options?.guide && result.updatesAvailable > 0) {
2927
- const cwd = options.cwd ?? process.cwd();
2928
- const migrationsDir = findMigrationDocsDir(cwd);
2929
- if (migrationsDir) {
2930
- lines.push("", "=".repeat(60), "", "Migration Guide", "");
2931
- for (const pkg of result.packages) {
2932
- if (!(pkg.updateAvailable && pkg.latest))
2933
- continue;
2934
- const shortName = pkg.name.replace("@outfitter/", "");
2935
- const docs = readMigrationDocs(migrationsDir, shortName, pkg.current, pkg.latest);
2936
- for (const doc of docs) {
2937
- lines.push(doc, "", "---", "");
2938
- }
2939
- }
2940
- } else {
2941
- lines.push("", theme.muted("Migration docs not found locally. See https://github.com/outfitter-dev/outfitter for migration guides."));
2942
- }
2943
- }
2944
- await output7(lines);
2945
- }
2946
-
2947
- // src/actions.ts
2948
- var outputModeSchema = z.enum(["human", "json", "jsonl"]).optional();
2949
- var initInputSchema = z.object({
2950
- targetDir: z.string(),
2951
- name: z.string().optional(),
2952
- bin: z.string().optional(),
2953
- template: z.string().optional(),
2954
- local: z.boolean().optional(),
2955
- force: z.boolean(),
2956
- with: z.string().optional(),
2957
- noTooling: z.boolean().optional(),
2958
- outputMode: outputModeSchema
2959
- });
2960
- var createInputSchema = z.object({
2961
- targetDir: z.string(),
2962
- name: z.string().optional(),
2963
- preset: z.enum(["basic", "cli", "daemon", "mcp"]).optional(),
2964
- structure: z.enum(["single", "workspace"]).optional(),
2965
- workspaceName: z.string().optional(),
2966
- local: z.boolean().optional(),
2967
- force: z.boolean(),
2968
- with: z.string().optional(),
2969
- noTooling: z.boolean().optional(),
2970
- yes: z.boolean().optional(),
2971
- outputMode: outputModeSchema
2972
- });
2973
- var migrateKitInputSchema = z.object({
2974
- targetDir: z.string(),
2975
- dryRun: z.boolean(),
2976
- outputMode: outputModeSchema
2977
- });
2978
- var doctorInputSchema = z.object({
2979
- cwd: z.string(),
2980
- outputMode: outputModeSchema
2981
- });
2982
- function resolveStringFlag(value) {
2983
- return typeof value === "string" && value.length > 0 ? value : undefined;
2984
- }
2985
- function resolveOutputMode(flags) {
2986
- if (flags["json"]) {
2987
- process.env["OUTFITTER_JSON"] = "1";
2988
- return "json";
2989
- }
2990
- return;
2991
- }
2992
- function resolveNoToolingFlag(flags) {
2993
- if (typeof flags.noTooling === "boolean") {
2994
- return !flags.noTooling;
2995
- }
2996
- if (typeof flags.tooling === "boolean") {
2997
- if (!flags.tooling) {
2998
- return true;
2999
- }
3000
- return process.argv.includes("--tooling") ? false : undefined;
3001
- }
3002
- return;
3003
- }
3004
- function resolveLocalFlag(flags) {
3005
- if (flags.local === true || flags.workspace === true) {
3006
- return true;
3007
- }
3008
- return;
3009
- }
3010
- function resolveInitOptions(context, templateOverride) {
3011
- const flags = context.flags;
3012
- const targetDir = context.args[0] ?? process.cwd();
3013
- const name = resolveStringFlag(flags.name);
3014
- const bin = resolveStringFlag(flags.bin);
3015
- const template = templateOverride ?? resolveStringFlag(flags.template);
3016
- const local = resolveLocalFlag(flags);
3017
- const force = Boolean(flags.force);
3018
- const withBlocks = resolveStringFlag(flags.with);
3019
- const noTooling = resolveNoToolingFlag(flags);
3020
- const outputMode = resolveOutputMode(context.flags);
3021
- return {
3022
- targetDir,
3023
- name,
3024
- template,
3025
- force,
3026
- ...local !== undefined ? { local } : {},
3027
- ...withBlocks ? { with: withBlocks } : {},
3028
- ...noTooling !== undefined ? { noTooling } : {},
3029
- ...bin ? { bin } : {},
3030
- ...outputMode ? { outputMode } : {}
3031
- };
3032
- }
3033
- function resolveCreateOptions(context) {
3034
- const flags = context.flags;
3035
- const outputMode = resolveOutputMode(context.flags);
3036
- const noTooling = resolveNoToolingFlag(flags);
3037
- const local = resolveLocalFlag(flags);
3038
- return {
3039
- targetDir: context.args[0] ?? process.cwd(),
3040
- name: resolveStringFlag(flags.name),
3041
- preset: resolveStringFlag(flags.preset),
3042
- structure: resolveStringFlag(flags.structure),
3043
- workspaceName: resolveStringFlag(flags.workspaceName),
3044
- force: Boolean(flags.force),
3045
- ...local !== undefined ? { local } : {},
3046
- with: resolveStringFlag(flags.with),
3047
- ...noTooling !== undefined ? { noTooling } : {},
3048
- yes: Boolean(flags.yes),
3049
- ...outputMode ? { outputMode } : {}
3050
- };
3051
- }
3052
- function resolveMigrateKitOptions(context) {
3053
- const flags = context.flags;
3054
- const outputMode = resolveOutputMode(context.flags);
3055
- return {
3056
- targetDir: context.args[0] ?? process.cwd(),
3057
- dryRun: Boolean(flags.dryRun || context.flags["dry-run"]),
3058
- ...outputMode ? { outputMode } : {}
3059
- };
3060
- }
3061
- var commonInitOptions = [
3062
- {
3063
- flags: "-n, --name <name>",
3064
- description: "Package name (defaults to directory name)"
3065
- },
3066
- {
3067
- flags: "-b, --bin <name>",
3068
- description: "Binary name (defaults to project name)"
3069
- },
3070
- {
3071
- flags: "-f, --force",
3072
- description: "Overwrite existing files",
3073
- defaultValue: false
3074
- },
3075
- {
3076
- flags: "--local",
3077
- description: "Use workspace:* for @outfitter dependencies"
3078
- },
3079
- {
3080
- flags: "--workspace",
3081
- description: "Alias for --local"
3082
- },
3083
- {
3084
- flags: "--with <blocks>",
3085
- description: "Tooling to add (comma-separated: scaffolding, claude, biome, lefthook, bootstrap)"
3086
- },
3087
- {
3088
- flags: "--no-tooling",
3089
- description: "Skip tooling setup"
3090
- }
3091
- ];
3092
- var templateOption = {
3093
- flags: "-t, --template <template>",
3094
- description: "Template to use"
3095
- };
3096
- function createInitAction(options) {
3097
- const initOptions = options.includeTemplateOption ? [...commonInitOptions, templateOption] : commonInitOptions;
3098
- return defineAction({
3099
- id: options.id,
3100
- description: options.description,
3101
- surfaces: ["cli"],
3102
- input: initInputSchema,
3103
- cli: {
3104
- group: "init",
3105
- command: options.command,
3106
- description: options.description,
3107
- options: initOptions,
3108
- mapInput: (context) => resolveInitOptions(context, options.templateOverride)
3109
- },
3110
- handler: async (input) => {
3111
- const { outputMode, ...initInput } = input;
3112
- const outputOptions = outputMode ? { mode: outputMode } : undefined;
3113
- const result = await runInit(initInput);
3114
- if (result.isErr()) {
3115
- return Result7.err(new InternalError2({
3116
- message: result.error.message,
3117
- context: { action: options.id }
3118
- }));
3119
- }
3120
- await printInitResults(initInput.targetDir, result.value, outputOptions);
3121
- return Result7.ok({ ok: true });
3122
- }
3123
- });
3124
- }
3125
- var createAction = defineAction({
3126
- id: "create",
3127
- description: "Interactive scaffolding flow for Outfitter projects (single package or workspace)",
3128
- surfaces: ["cli"],
3129
- input: createInputSchema,
3130
- cli: {
3131
- command: "create [directory]",
3132
- description: "Interactive scaffolding flow for Outfitter projects (single package or workspace)",
3133
- options: [
3134
- {
3135
- flags: "-n, --name <name>",
3136
- description: "Package name"
3137
- },
3138
- {
3139
- flags: "-p, --preset <preset>",
3140
- description: "Preset to scaffold (basic, cli, daemon, mcp)"
3141
- },
3142
- {
3143
- flags: "-s, --structure <structure>",
3144
- description: "Project structure (single|workspace)"
3145
- },
3146
- {
3147
- flags: "--workspace-name <name>",
3148
- description: "Workspace root package name"
3149
- },
3150
- {
3151
- flags: "--local",
3152
- description: "Use workspace:* for @outfitter dependencies"
3153
- },
3154
- {
3155
- flags: "--workspace",
3156
- description: "Alias for --local"
3157
- },
3158
- {
3159
- flags: "-f, --force",
3160
- description: "Overwrite existing files",
3161
- defaultValue: false
3162
- },
3163
- {
3164
- flags: "--with <blocks>",
3165
- description: "Comma-separated tooling blocks to add"
3166
- },
3167
- {
3168
- flags: "--no-tooling",
3169
- description: "Skip default tooling blocks"
3170
- },
3171
- {
3172
- flags: "-y, --yes",
3173
- description: "Skip prompts and use defaults for missing values",
3174
- defaultValue: false
3175
- }
3176
- ],
3177
- mapInput: resolveCreateOptions
3178
- },
3179
- handler: async (input) => {
3180
- const { outputMode, ...createInput } = input;
3181
- const outputOptions = outputMode ? { mode: outputMode } : undefined;
3182
- const result = await runCreate(createInput);
3183
- if (result.isErr()) {
3184
- return Result7.err(new InternalError2({
3185
- message: result.error.message,
3186
- context: { action: "create" }
3187
- }));
3188
- }
3189
- await printCreateResults(result.value, outputOptions);
3190
- return Result7.ok(result.value);
3191
- }
3192
- });
3193
- var demoInputSchema = z.object({
3194
- section: z.string().optional(),
3195
- list: z.boolean().optional(),
3196
- animate: z.boolean().optional()
3197
- });
3198
- var demoAction = defineAction({
3199
- id: "demo",
3200
- description: "Showcase @outfitter/cli rendering capabilities",
3201
- surfaces: ["cli"],
3202
- input: demoInputSchema,
3203
- cli: {
3204
- command: "demo [section]",
3205
- description: "Showcase @outfitter/cli rendering capabilities",
3206
- options: [
3207
- {
3208
- flags: "-l, --list",
3209
- description: "List available demo sections",
3210
- defaultValue: false
3211
- },
3212
- {
3213
- flags: "-a, --animate",
3214
- description: "Run animated demo (spinners only)",
3215
- defaultValue: false
3216
- }
3217
- ],
3218
- mapInput: (context) => {
3219
- resolveOutputMode(context.flags);
3220
- return {
3221
- section: context.args[0],
3222
- list: Boolean(context.flags["list"]),
3223
- animate: Boolean(context.flags["animate"])
3224
- };
3225
- }
3226
- },
3227
- handler: async (input) => {
3228
- const result = await runDemo(input);
3229
- await printDemoResults(result);
3230
- if (result.exitCode !== 0) {
3231
- process.exit(result.exitCode);
3232
- }
3233
- return Result7.ok(result);
3234
- }
3235
- });
3236
- var doctorAction = defineAction({
3237
- id: "doctor",
3238
- description: "Validate environment and dependencies",
3239
- surfaces: ["cli"],
3240
- input: doctorInputSchema,
3241
- cli: {
3242
- command: "doctor",
3243
- description: "Validate environment and dependencies",
3244
- mapInput: (context) => {
3245
- const outputMode = resolveOutputMode(context.flags);
3246
- return {
3247
- cwd: process.cwd(),
3248
- ...outputMode ? { outputMode } : {}
3249
- };
3250
- }
3251
- },
3252
- handler: async (input) => {
3253
- const { outputMode, ...doctorInput } = input;
3254
- const outputOptions = outputMode ? { mode: outputMode } : undefined;
3255
- const result = await runDoctor(doctorInput);
3256
- await printDoctorResults(result, outputOptions);
3257
- if (result.exitCode !== 0) {
3258
- process.exit(result.exitCode);
3259
- }
3260
- return Result7.ok(result);
3261
- }
3262
- });
3263
- var addInputSchema = z.object({
3264
- block: z.string(),
3265
- force: z.boolean(),
3266
- dryRun: z.boolean(),
3267
- cwd: z.string().optional(),
3268
- outputMode: outputModeSchema
3269
- });
3270
- var addAction = defineAction({
3271
- id: "add",
3272
- description: "Add a block from the registry to your project",
3273
- surfaces: ["cli"],
3274
- input: addInputSchema,
3275
- cli: {
3276
- group: "add",
3277
- command: "<block>",
3278
- description: "Add a block from the registry (claude, biome, lefthook, bootstrap, scaffolding)",
3279
- options: [
3280
- {
3281
- flags: "-f, --force",
3282
- description: "Overwrite existing files",
3283
- defaultValue: false
3284
- },
3285
- {
3286
- flags: "--dry-run",
3287
- description: "Show what would be added without making changes",
3288
- defaultValue: false
3289
- }
3290
- ],
3291
- mapInput: (context) => {
3292
- const outputMode = resolveOutputMode(context.flags);
3293
- return {
3294
- block: context.args[0],
3295
- force: Boolean(context.flags["force"]),
3296
- dryRun: Boolean(context.flags["dry-run"] ?? context.flags["dryRun"]),
3297
- cwd: process.cwd(),
3298
- ...outputMode ? { outputMode } : {}
3299
- };
3300
- }
3301
- },
3302
- handler: async (input) => {
3303
- const { outputMode, ...addInput } = input;
3304
- const outputOptions = outputMode ? { mode: outputMode } : undefined;
3305
- const result = await runAdd(addInput);
3306
- if (result.isErr()) {
3307
- return Result7.err(new InternalError2({
3308
- message: result.error.message,
3309
- context: { action: "add" }
3310
- }));
3311
- }
3312
- await printAddResults(result.value, addInput.dryRun, outputOptions);
3313
- return Result7.ok(result.value);
3314
- }
3315
- });
3316
- var listBlocksAction = defineAction({
3317
- id: "add.list",
3318
- description: "List available blocks",
3319
- surfaces: ["cli"],
3320
- input: z.object({ outputMode: outputModeSchema }),
3321
- cli: {
3322
- group: "add",
3323
- command: "list",
3324
- description: "List available blocks",
3325
- mapInput: (context) => {
3326
- const outputMode = resolveOutputMode(context.flags);
3327
- return {
3328
- ...outputMode ? { outputMode } : {}
3329
- };
3330
- }
3331
- },
3332
- handler: async (input) => {
3333
- const result = listBlocks();
3334
- if (result.isErr()) {
3335
- return Result7.err(new InternalError2({
3336
- message: result.error.message,
3337
- context: { action: "add.list" }
3338
- }));
3339
- }
3340
- if (input.outputMode === "json" || input.outputMode === "jsonl") {
3341
- await output8({ blocks: result.value }, { mode: input.outputMode });
3342
- } else {
3343
- const lines = [
3344
- "Available blocks:",
3345
- ...result.value.map((block) => ` - ${block}`)
3346
- ];
3347
- await output8(lines);
3348
- }
3349
- return Result7.ok({ blocks: result.value });
3350
- }
3351
- });
3352
- var updateInputSchema = z.object({
3353
- cwd: z.string(),
3354
- guide: z.boolean(),
3355
- outputMode: outputModeSchema
3356
- });
3357
- var updateAction = defineAction({
3358
- id: "update",
3359
- description: "Check for @outfitter/* package updates and migration guidance",
3360
- surfaces: ["cli"],
3361
- input: updateInputSchema,
3362
- cli: {
3363
- command: "update",
3364
- description: "Check for @outfitter/* package updates and migration guidance",
3365
- options: [
3366
- {
3367
- flags: "--guide",
3368
- description: "Show migration instructions for available updates",
3369
- defaultValue: false
3370
- },
3371
- {
3372
- flags: "--cwd <path>",
3373
- description: "Working directory (defaults to current directory)"
3374
- }
3375
- ],
3376
- mapInput: (context) => {
3377
- const outputMode = resolveOutputMode(context.flags);
3378
- const cwd = typeof context.flags["cwd"] === "string" ? resolve7(process.cwd(), context.flags["cwd"]) : process.cwd();
3379
- return {
3380
- cwd,
3381
- guide: Boolean(context.flags["guide"]),
3382
- ...outputMode ? { outputMode } : {}
3383
- };
3384
- }
3385
- },
3386
- handler: async (input) => {
3387
- const { outputMode, ...updateInput } = input;
3388
- const result = await runUpdate(updateInput);
3389
- if (result.isErr()) {
3390
- return Result7.err(new InternalError2({
3391
- message: result.error.message,
3392
- context: { action: "update" }
3393
- }));
3394
- }
3395
- await printUpdateResults(result.value, {
3396
- ...outputMode ? { mode: outputMode } : {},
3397
- guide: updateInput.guide,
3398
- cwd: updateInput.cwd
3399
- });
3400
- return Result7.ok(result.value);
3401
- }
3402
- });
3403
- var migrateKitAction = defineAction({
3404
- id: "migrate.kit",
3405
- description: "Migrate foundation imports and dependencies to @outfitter/kit",
3406
- surfaces: ["cli"],
3407
- input: migrateKitInputSchema,
3408
- cli: {
3409
- group: "migrate",
3410
- command: "kit [directory]",
3411
- description: "Migrate foundation imports and dependencies to @outfitter/kit",
3412
- options: [
3413
- {
3414
- flags: "--dry-run",
3415
- description: "Preview changes without writing files",
3416
- defaultValue: false
3417
- }
3418
- ],
3419
- mapInput: resolveMigrateKitOptions
3420
- },
3421
- handler: async (input) => {
3422
- const { outputMode, ...migrateInput } = input;
3423
- const outputOptions = outputMode ? { mode: outputMode } : undefined;
3424
- const result = await runMigrateKit(migrateInput);
3425
- if (result.isErr()) {
3426
- return Result7.err(new InternalError2({
3427
- message: result.error.message,
3428
- context: { action: "migrate.kit" }
3429
- }));
3430
- }
3431
- await printMigrateKitResults(result.value, outputOptions);
3432
- return Result7.ok(result.value);
3433
- }
3434
- });
3435
- var outfitterActions = createActionRegistry().add(createAction).add(createInitAction({
3436
- id: "init",
3437
- description: "Scaffold a new Outfitter project",
3438
- command: "[directory]",
3439
- includeTemplateOption: true
3440
- })).add(createInitAction({
3441
- id: "init.cli",
3442
- description: "Scaffold a new CLI project",
3443
- command: "cli [directory]",
3444
- templateOverride: "cli"
3445
- })).add(createInitAction({
3446
- id: "init.mcp",
3447
- description: "Scaffold a new MCP server",
3448
- command: "mcp [directory]",
3449
- templateOverride: "mcp"
3450
- })).add(createInitAction({
3451
- id: "init.daemon",
3452
- description: "Scaffold a new daemon project",
3453
- command: "daemon [directory]",
3454
- templateOverride: "daemon"
3455
- })).add(demoAction).add(doctorAction).add(addAction).add(listBlocksAction).add(migrateKitAction).add(updateAction);
3456
-
3457
- export { CREATE_PRESETS, CREATE_PRESET_IDS, getCreatePreset, planCreateProject, CreateError, runCreate, printCreateResults, createCommand, runDoctor, printDoctorResults, doctorCommand, InitError, runInit, initCommand, MigrateKitError, runMigrateKit, printMigrateKitResults, migrateKitCommand, outfitterActions };