outfitter 0.1.0-rc.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import {
3
3
  outfitterActions
4
- } from "./shared/chunk-1rebv144.js";
4
+ } from "./shared/chunk-429agb93.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { buildCliCommands } from "@outfitter/cli/actions";
package/dist/index.d.ts CHANGED
@@ -128,6 +128,7 @@ declare function printDoctorResults(result: DoctorResult): void;
128
128
  */
129
129
  declare function doctorCommand(program: Command): void;
130
130
  import { Result } from "@outfitter/contracts";
131
+ import { AddBlockResult } from "@outfitter/tooling";
131
132
  import { Command as Command2 } from "commander";
132
133
  /**
133
134
  * Options for the init command.
@@ -138,13 +139,24 @@ interface InitOptions {
138
139
  /** Package name (defaults to directory name if not provided) */
139
140
  readonly name: string | undefined;
140
141
  /** Binary name (defaults to project name if not provided) */
141
- readonly bin?: string;
142
+ readonly bin?: string | undefined;
142
143
  /** Template to use (defaults to 'basic') */
143
144
  readonly template: string | undefined;
144
145
  /** Whether to use local/workspace dependencies */
145
- readonly local?: boolean;
146
+ readonly local?: boolean | undefined;
146
147
  /** Whether to overwrite existing files */
147
148
  readonly force: boolean;
149
+ /** Tooling blocks to add (e.g., "scaffolding" or "claude,biome,lefthook") */
150
+ readonly with?: string | undefined;
151
+ /** Skip tooling prompt in interactive mode */
152
+ readonly noTooling?: boolean | undefined;
153
+ }
154
+ /**
155
+ * Result of running init, including any blocks added.
156
+ */
157
+ interface InitResult {
158
+ /** The blocks that were added, if any */
159
+ readonly blocksAdded?: AddBlockResult | undefined;
148
160
  }
149
161
  /**
150
162
  * Error returned when initialization fails.
@@ -166,16 +178,20 @@ declare class InitError extends Error {
166
178
  * name: "my-project",
167
179
  * template: "basic",
168
180
  * force: false,
181
+ * blocks: "scaffolding", // Optional: add tooling blocks
169
182
  * });
170
183
  *
171
184
  * if (result.isOk()) {
172
185
  * console.log("Project initialized successfully!");
186
+ * if (result.value.blocksAdded) {
187
+ * console.log(`Added ${result.value.blocksAdded.created.length} tooling files`);
188
+ * }
173
189
  * } else {
174
190
  * console.error("Failed:", result.error.message);
175
191
  * }
176
192
  * ```
177
193
  */
178
- declare function runInit(options: InitOptions): Promise<Result<void, InitError>>;
194
+ declare function runInit(options: InitOptions): Promise<Result<InitResult, InitError>>;
179
195
  /**
180
196
  * Registers the init command with the CLI program.
181
197
  *
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  printDoctorResults,
7
7
  runDoctor,
8
8
  runInit
9
- } from "./shared/chunk-1rebv144.js";
9
+ } from "./shared/chunk-429agb93.js";
10
10
  export {
11
11
  runInit,
12
12
  runDoctor,
@@ -186,21 +186,249 @@ function doctorCommand(program) {
186
186
 
187
187
  // src/commands/init.ts
188
188
  import {
189
+ existsSync as existsSync3,
190
+ mkdirSync as mkdirSync2,
191
+ readdirSync,
192
+ readFileSync as readFileSync3,
193
+ statSync,
194
+ writeFileSync as writeFileSync2
195
+ } from "node:fs";
196
+ import { basename, dirname as dirname2, extname, join as join3, resolve as resolve3 } from "node:path";
197
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
198
+ import { Result as Result2 } from "@outfitter/contracts";
199
+
200
+ // src/commands/add.ts
201
+ import {
202
+ chmodSync,
189
203
  existsSync as existsSync2,
190
204
  mkdirSync,
191
- readdirSync,
192
205
  readFileSync as readFileSync2,
193
- statSync,
194
206
  writeFileSync
195
207
  } from "node:fs";
196
- import { basename, dirname, extname, join as join2, resolve as resolve2 } from "node:path";
208
+ import { dirname, join as join2, resolve as resolve2 } from "node:path";
197
209
  import { fileURLToPath } from "node:url";
198
- import { confirm, isCancel, select, text } from "@clack/prompts";
199
210
  import { Result } from "@outfitter/contracts";
211
+ import { RegistrySchema } from "@outfitter/tooling";
212
+
213
+ class AddError extends Error {
214
+ _tag = "AddError";
215
+ constructor(message) {
216
+ super(message);
217
+ this.name = "AddError";
218
+ }
219
+ }
220
+ function getRegistryPath() {
221
+ let currentDir = dirname(fileURLToPath(import.meta.url));
222
+ for (let i = 0;i < 10; i++) {
223
+ const registryPath = join2(currentDir, "node_modules/@outfitter/tooling/registry/registry.json");
224
+ if (existsSync2(registryPath)) {
225
+ return registryPath;
226
+ }
227
+ const monoRepoPath = join2(currentDir, "packages/tooling/registry/registry.json");
228
+ if (existsSync2(monoRepoPath)) {
229
+ return monoRepoPath;
230
+ }
231
+ currentDir = dirname(currentDir);
232
+ }
233
+ throw new AddError("Could not find registry.json. Ensure @outfitter/tooling is installed.");
234
+ }
235
+ function loadRegistry() {
236
+ try {
237
+ const registryPath = getRegistryPath();
238
+ const content = readFileSync2(registryPath, "utf-8");
239
+ const parsed = JSON.parse(content);
240
+ const registry = RegistrySchema.parse(parsed);
241
+ return Result.ok(registry);
242
+ } catch (error) {
243
+ const message = error instanceof Error ? error.message : "Unknown error";
244
+ return Result.err(new AddError(`Failed to load registry: ${message}`));
245
+ }
246
+ }
247
+ function resolveBlock(registry, blockName, visited = new Set) {
248
+ if (visited.has(blockName)) {
249
+ return Result.err(new AddError(`Circular dependency detected for block: ${blockName}`));
250
+ }
251
+ visited.add(blockName);
252
+ const block = registry.blocks[blockName];
253
+ if (!block) {
254
+ const available = Object.keys(registry.blocks).join(", ");
255
+ return Result.err(new AddError(`Block '${blockName}' not found. Available blocks: ${available}`));
256
+ }
257
+ if (block.extends && block.extends.length > 0) {
258
+ const mergedFiles = [];
259
+ const mergedDeps = {};
260
+ const mergedDevDeps = {};
261
+ for (const extendedName of block.extends) {
262
+ const extendedResult = resolveBlock(registry, extendedName, visited);
263
+ if (extendedResult.isErr()) {
264
+ return extendedResult;
265
+ }
266
+ const extended = extendedResult.value;
267
+ if (extended.files) {
268
+ mergedFiles.push(...extended.files);
269
+ }
270
+ if (extended.dependencies) {
271
+ Object.assign(mergedDeps, extended.dependencies);
272
+ }
273
+ if (extended.devDependencies) {
274
+ Object.assign(mergedDevDeps, extended.devDependencies);
275
+ }
276
+ }
277
+ if (block.files) {
278
+ mergedFiles.push(...block.files);
279
+ }
280
+ if (block.dependencies) {
281
+ Object.assign(mergedDeps, block.dependencies);
282
+ }
283
+ if (block.devDependencies) {
284
+ Object.assign(mergedDevDeps, block.devDependencies);
285
+ }
286
+ return Result.ok({
287
+ name: block.name,
288
+ description: block.description,
289
+ files: mergedFiles.length > 0 ? mergedFiles : undefined,
290
+ dependencies: Object.keys(mergedDeps).length > 0 ? mergedDeps : undefined,
291
+ devDependencies: Object.keys(mergedDevDeps).length > 0 ? mergedDevDeps : undefined
292
+ });
293
+ }
294
+ return Result.ok(block);
295
+ }
296
+ function writeFile(filePath, content, executable) {
297
+ const dir = dirname(filePath);
298
+ if (!existsSync2(dir)) {
299
+ mkdirSync(dir, { recursive: true });
300
+ }
301
+ writeFileSync(filePath, content, "utf-8");
302
+ if (executable) {
303
+ chmodSync(filePath, 493);
304
+ }
305
+ }
306
+ function updatePackageJson(cwd, dependencies, devDependencies, dryRun) {
307
+ const packageJsonPath = join2(cwd, "package.json");
308
+ if (!existsSync2(packageJsonPath)) {
309
+ return { dependencies, devDependencies };
310
+ }
311
+ const content = readFileSync2(packageJsonPath, "utf-8");
312
+ const pkg = JSON.parse(content);
313
+ const existingDeps = pkg["dependencies"] ?? {};
314
+ const existingDevDeps = pkg["devDependencies"] ?? {};
315
+ const addedDeps = {};
316
+ const addedDevDeps = {};
317
+ for (const [name, version] of Object.entries(dependencies)) {
318
+ if (!existingDeps[name]) {
319
+ existingDeps[name] = version;
320
+ addedDeps[name] = version;
321
+ }
322
+ }
323
+ for (const [name, version] of Object.entries(devDependencies)) {
324
+ if (!(existingDevDeps[name] || existingDeps[name])) {
325
+ existingDevDeps[name] = version;
326
+ addedDevDeps[name] = version;
327
+ }
328
+ }
329
+ if (!dryRun && (Object.keys(addedDeps).length > 0 || Object.keys(addedDevDeps).length > 0)) {
330
+ if (Object.keys(existingDeps).length > 0) {
331
+ pkg["dependencies"] = existingDeps;
332
+ }
333
+ if (Object.keys(existingDevDeps).length > 0) {
334
+ pkg["devDependencies"] = existingDevDeps;
335
+ }
336
+ writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
337
+ `);
338
+ }
339
+ return { dependencies: addedDeps, devDependencies: addedDevDeps };
340
+ }
341
+ async function runAdd(input) {
342
+ const { block: blockName, force, dryRun, cwd = process.cwd() } = input;
343
+ const resolvedCwd = resolve2(cwd);
344
+ const registryResult = loadRegistry();
345
+ if (registryResult.isErr()) {
346
+ return registryResult;
347
+ }
348
+ const registry = registryResult.value;
349
+ const blockResult = resolveBlock(registry, blockName);
350
+ if (blockResult.isErr()) {
351
+ return blockResult;
352
+ }
353
+ const block = blockResult.value;
354
+ const created = [];
355
+ const skipped = [];
356
+ const overwritten = [];
357
+ if (block.files) {
358
+ for (const file of block.files) {
359
+ const targetPath = join2(resolvedCwd, file.path);
360
+ const fileExists = existsSync2(targetPath);
361
+ if (fileExists && !force) {
362
+ skipped.push(file.path);
363
+ continue;
364
+ }
365
+ if (!dryRun) {
366
+ writeFile(targetPath, file.content, file.executable ?? false);
367
+ }
368
+ if (fileExists) {
369
+ overwritten.push(file.path);
370
+ } else {
371
+ created.push(file.path);
372
+ }
373
+ }
374
+ }
375
+ const { dependencies, devDependencies } = updatePackageJson(resolvedCwd, block.dependencies ?? {}, block.devDependencies ?? {}, dryRun);
376
+ return Result.ok({
377
+ created,
378
+ skipped,
379
+ overwritten,
380
+ dependencies,
381
+ devDependencies
382
+ });
383
+ }
384
+ function printAddResults(result, dryRun) {
385
+ const prefix = dryRun ? "[dry-run] Would " : "";
386
+ if (result.created.length > 0) {
387
+ console.log(`${prefix}create ${result.created.length} file(s):`);
388
+ for (const file of result.created) {
389
+ console.log(` ✓ ${file}`);
390
+ }
391
+ }
392
+ if (result.overwritten.length > 0) {
393
+ console.log(`${prefix}overwrite ${result.overwritten.length} file(s):`);
394
+ for (const file of result.overwritten) {
395
+ console.log(` ✓ ${file}`);
396
+ }
397
+ }
398
+ if (result.skipped.length > 0) {
399
+ console.log(`Skipped ${result.skipped.length} existing file(s):`);
400
+ for (const file of result.skipped) {
401
+ console.log(` - ${file} (use --force to overwrite)`);
402
+ }
403
+ }
404
+ const depCount = Object.keys(result.dependencies).length + Object.keys(result.devDependencies).length;
405
+ if (depCount > 0) {
406
+ console.log(`
407
+ ${prefix}add ${depCount} package(s) to package.json:`);
408
+ for (const [name, version] of Object.entries(result.dependencies)) {
409
+ console.log(` + ${name}@${version}`);
410
+ }
411
+ for (const [name, version] of Object.entries(result.devDependencies)) {
412
+ console.log(` + ${name}@${version} (dev)`);
413
+ }
414
+ if (!dryRun) {
415
+ console.log("\nRun `bun install` to install new dependencies.");
416
+ }
417
+ }
418
+ }
419
+ function listBlocks() {
420
+ const registryResult = loadRegistry();
421
+ if (registryResult.isErr()) {
422
+ return registryResult;
423
+ }
424
+ const blocks = Object.keys(registryResult.value.blocks);
425
+ return Result.ok(blocks);
426
+ }
200
427
 
201
428
  // src/commands/shared-deps.ts
202
429
  var SHARED_DEV_DEPS = {
203
430
  "@biomejs/biome": "^2.3.11",
431
+ "@outfitter/tooling": "^0.1.0-rc.1",
204
432
  "@types/bun": "latest",
205
433
  lefthook: "^2.0.15",
206
434
  typescript: "^5.9.3",
@@ -264,23 +492,23 @@ function isBinaryFile(filename) {
264
492
  return BINARY_EXTENSIONS.has(ext);
265
493
  }
266
494
  function getTemplatesDir() {
267
- let currentDir = dirname(fileURLToPath(import.meta.url));
495
+ let currentDir = dirname2(fileURLToPath2(import.meta.url));
268
496
  for (let i = 0;i < 10; i++) {
269
- const templatesPath = join2(currentDir, "templates");
270
- if (existsSync2(templatesPath)) {
497
+ const templatesPath = join3(currentDir, "templates");
498
+ if (existsSync3(templatesPath)) {
271
499
  return templatesPath;
272
500
  }
273
- currentDir = dirname(currentDir);
501
+ currentDir = dirname2(currentDir);
274
502
  }
275
- return join2(process.cwd(), "templates");
503
+ return join3(process.cwd(), "templates");
276
504
  }
277
505
  function validateTemplate(templateName) {
278
506
  const templatesDir = getTemplatesDir();
279
- const templatePath = join2(templatesDir, templateName);
280
- if (!existsSync2(templatePath)) {
281
- return Result.err(new InitError(`Template '${templateName}' not found. Available templates are in: ${templatesDir}`));
507
+ const templatePath = join3(templatesDir, templateName);
508
+ if (!existsSync3(templatePath)) {
509
+ return Result2.err(new InitError(`Template '${templateName}' not found. Available templates are in: ${templatesDir}`));
282
510
  }
283
- return Result.ok(templatePath);
511
+ return Result2.ok(templatePath);
284
512
  }
285
513
  function replacePlaceholders(content, values) {
286
514
  return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
@@ -296,44 +524,8 @@ function getOutputFilename(templateFilename) {
296
524
  }
297
525
  return templateFilename;
298
526
  }
299
- function isInteractive() {
300
- return process.stdout.isTTY === true && process.env["TERM"] !== "dumb";
301
- }
302
- function readPackageInfo(targetDir) {
303
- const packageJsonPath = join2(targetDir, "package.json");
304
- if (!existsSync2(packageJsonPath)) {
305
- return {};
306
- }
307
- try {
308
- const content = readFileSync2(packageJsonPath, "utf-8");
309
- const parsed = JSON.parse(content);
310
- const name = parsed["name"];
311
- const resolvedName = typeof name === "string" && name.length > 0 ? name : undefined;
312
- const bin = parsed["bin"];
313
- let resolvedBin;
314
- if (typeof bin === "string") {
315
- resolvedBin = bin.length > 0 ? bin : undefined;
316
- } else if (typeof bin === "object" && bin !== null) {
317
- const entries = Object.entries(bin).filter(([, value]) => typeof value === "string");
318
- const keys = entries.map(([key]) => key);
319
- if (keys.length > 0) {
320
- const derived = resolvedName ? deriveBinName(resolvedName) : undefined;
321
- if (derived && keys.includes(derived)) {
322
- resolvedBin = derived;
323
- } else if (resolvedName && keys.includes(resolvedName)) {
324
- resolvedBin = resolvedName;
325
- } else {
326
- resolvedBin = keys[0];
327
- }
328
- }
329
- }
330
- return {
331
- ...resolvedName ? { name: resolvedName } : {},
332
- ...resolvedBin ? { bin: resolvedBin } : {}
333
- };
334
- } catch {
335
- return {};
336
- }
527
+ function hasPackageJson(targetDir) {
528
+ return existsSync3(join3(targetDir, "package.json"));
337
529
  }
338
530
  function deriveProjectName(packageName) {
339
531
  if (packageName.startsWith("@")) {
@@ -344,95 +536,14 @@ function deriveProjectName(packageName) {
344
536
  }
345
537
  return packageName;
346
538
  }
347
- function deriveBinName(projectName) {
348
- if (projectName.startsWith("@")) {
349
- const parts = projectName.split("/");
350
- if (parts.length > 1 && parts[1]) {
351
- return parts[1];
352
- }
353
- }
354
- return projectName;
539
+ function resolvePackageName(options, resolvedTargetDir) {
540
+ return options.name ?? basename(resolvedTargetDir);
355
541
  }
356
- async function resolvePackageName(options, resolvedTargetDir, packageInfo) {
357
- if (options.name) {
358
- return Result.ok(options.name);
359
- }
360
- const detectedName = packageInfo.name;
361
- const fallbackName = basename(resolvedTargetDir);
362
- if (!isInteractive()) {
363
- return Result.ok(detectedName ?? fallbackName);
364
- }
365
- const suggested = detectedName ?? fallbackName;
366
- const custom = await text({
367
- message: "Package name",
368
- placeholder: suggested
369
- });
370
- if (isCancel(custom)) {
371
- return Result.err(new InitError("Initialization cancelled."));
372
- }
373
- const trimmed = String(custom).trim();
374
- return Result.ok(trimmed.length > 0 ? trimmed : suggested);
375
- }
376
- async function resolveBinName(options, projectName, packageInfo) {
377
- if (options.bin) {
378
- return Result.ok(options.bin);
379
- }
380
- const derived = deriveBinName(projectName);
381
- const detected = packageInfo.bin;
382
- if (!isInteractive()) {
383
- return Result.ok(detected ?? derived);
384
- }
385
- if (detected) {
386
- const useDetected = await confirm({
387
- message: `Detected package binary name "${detected}". Use this as the binary name?`
388
- });
389
- if (isCancel(useDetected)) {
390
- return Result.err(new InitError("Initialization cancelled."));
391
- }
392
- if (useDetected) {
393
- return Result.ok(detected);
394
- }
395
- }
396
- const useDerived = await confirm({
397
- message: `Use "${derived}" as the binary name?`
398
- });
399
- if (isCancel(useDerived)) {
400
- return Result.err(new InitError("Initialization cancelled."));
401
- }
402
- if (useDerived) {
403
- return Result.ok(derived);
404
- }
405
- const custom = await text({
406
- message: "Binary name",
407
- placeholder: derived
408
- });
409
- if (isCancel(custom)) {
410
- return Result.err(new InitError("Initialization cancelled."));
411
- }
412
- const trimmed = String(custom).trim();
413
- return Result.ok(trimmed.length > 0 ? trimmed : derived);
542
+ function resolveBinName(options, projectName) {
543
+ return options.bin ?? deriveProjectName(projectName);
414
544
  }
415
- async function resolveTemplateName(options) {
416
- if (options.template) {
417
- return Result.ok(options.template);
418
- }
419
- if (!isInteractive()) {
420
- return Result.ok("basic");
421
- }
422
- const selection = await select({
423
- message: "Select a template",
424
- options: [
425
- { value: "basic", label: "basic", hint: "Minimal starter" },
426
- { value: "cli", label: "cli", hint: "CLI application" },
427
- { value: "mcp", label: "mcp", hint: "MCP server" },
428
- { value: "daemon", label: "daemon", hint: "Daemon + CLI" }
429
- ]
430
- });
431
- if (isCancel(selection)) {
432
- return Result.err(new InitError("Initialization cancelled."));
433
- }
434
- const value = String(selection).trim();
435
- return Result.ok(value.length > 0 ? value : "basic");
545
+ function resolveTemplateName(options) {
546
+ return options.template ?? "basic";
436
547
  }
437
548
  function resolveAuthor() {
438
549
  const fromEnv = process.env["GIT_AUTHOR_NAME"] ?? process.env["GIT_COMMITTER_NAME"] ?? process.env["AUTHOR"] ?? process.env["USER"] ?? process.env["USERNAME"];
@@ -454,41 +565,51 @@ function resolveAuthor() {
454
565
  function resolveYear() {
455
566
  return String(new Date().getFullYear());
456
567
  }
568
+ function resolveBlocks(options) {
569
+ if (options.noTooling) {
570
+ return;
571
+ }
572
+ if (options.with) {
573
+ const blocks = options.with.split(",").map((b) => b.trim()).filter(Boolean);
574
+ return blocks.length > 0 ? blocks : undefined;
575
+ }
576
+ return ["scaffolding"];
577
+ }
457
578
  function copyTemplateFiles(templateDir, targetDir, values, force, allowOverwrite = false) {
458
579
  try {
459
- if (!existsSync2(targetDir)) {
460
- mkdirSync(targetDir, { recursive: true });
580
+ if (!existsSync3(targetDir)) {
581
+ mkdirSync2(targetDir, { recursive: true });
461
582
  }
462
583
  const entries = readdirSync(templateDir);
463
584
  for (const entry of entries) {
464
- const sourcePath = join2(templateDir, entry);
585
+ const sourcePath = join3(templateDir, entry);
465
586
  const stat = statSync(sourcePath);
466
587
  if (stat.isDirectory()) {
467
- const targetSubDir = join2(targetDir, entry);
588
+ const targetSubDir = join3(targetDir, entry);
468
589
  const result = copyTemplateFiles(sourcePath, targetSubDir, values, force, allowOverwrite);
469
590
  if (result.isErr()) {
470
591
  return result;
471
592
  }
472
593
  } else if (stat.isFile()) {
473
594
  const outputFilename = getOutputFilename(entry);
474
- const targetPath = join2(targetDir, outputFilename);
475
- if (existsSync2(targetPath) && !force && !allowOverwrite) {
476
- return Result.err(new InitError(`File '${targetPath}' already exists. Use --force to overwrite.`));
595
+ const targetPath = join3(targetDir, outputFilename);
596
+ if (existsSync3(targetPath) && !force && !allowOverwrite) {
597
+ return Result2.err(new InitError(`File '${targetPath}' already exists. Use --force to overwrite.`));
477
598
  }
478
599
  if (isBinaryFile(outputFilename)) {
479
- const buffer = readFileSync2(sourcePath);
480
- writeFileSync(targetPath, buffer);
600
+ const buffer = readFileSync3(sourcePath);
601
+ writeFileSync2(targetPath, buffer);
481
602
  } else {
482
- const content = readFileSync2(sourcePath, "utf-8");
603
+ const content = readFileSync3(sourcePath, "utf-8");
483
604
  const processedContent = replacePlaceholders(content, values);
484
- writeFileSync(targetPath, processedContent, "utf-8");
605
+ writeFileSync2(targetPath, processedContent, "utf-8");
485
606
  }
486
607
  }
487
608
  }
488
- return Result.ok(undefined);
609
+ return Result2.ok(undefined);
489
610
  } catch (error) {
490
611
  const message = error instanceof Error ? error.message : "Unknown error";
491
- return Result.err(new InitError(`Failed to copy template files: ${message}`));
612
+ return Result2.err(new InitError(`Failed to copy template files: ${message}`));
492
613
  }
493
614
  }
494
615
  var DEPENDENCY_SECTIONS = [
@@ -498,12 +619,12 @@ var DEPENDENCY_SECTIONS = [
498
619
  "optionalDependencies"
499
620
  ];
500
621
  function rewriteLocalDependencies(targetDir) {
501
- const packageJsonPath = join2(targetDir, "package.json");
502
- if (!existsSync2(packageJsonPath)) {
503
- return Result.ok(undefined);
622
+ const packageJsonPath = join3(targetDir, "package.json");
623
+ if (!existsSync3(packageJsonPath)) {
624
+ return Result2.ok(undefined);
504
625
  }
505
626
  try {
506
- const content = readFileSync2(packageJsonPath, "utf-8");
627
+ const content = readFileSync3(packageJsonPath, "utf-8");
507
628
  const parsed = JSON.parse(content);
508
629
  let updated = false;
509
630
  for (const section of DEPENDENCY_SECTIONS) {
@@ -520,60 +641,50 @@ function rewriteLocalDependencies(targetDir) {
520
641
  }
521
642
  }
522
643
  if (updated) {
523
- writeFileSync(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
644
+ writeFileSync2(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
524
645
  `, "utf-8");
525
646
  }
526
- return Result.ok(undefined);
647
+ return Result2.ok(undefined);
527
648
  } catch (error) {
528
649
  const message = error instanceof Error ? error.message : "Unknown error";
529
- return Result.err(new InitError(`Failed to update local dependencies: ${message}`));
650
+ return Result2.err(new InitError(`Failed to update local dependencies: ${message}`));
530
651
  }
531
652
  }
532
653
  function injectSharedConfig(targetDir) {
533
- const packageJsonPath = join2(targetDir, "package.json");
534
- if (!existsSync2(packageJsonPath)) {
535
- return Result.ok(undefined);
654
+ const packageJsonPath = join3(targetDir, "package.json");
655
+ if (!existsSync3(packageJsonPath)) {
656
+ return Result2.ok(undefined);
536
657
  }
537
658
  try {
538
- const content = readFileSync2(packageJsonPath, "utf-8");
659
+ const content = readFileSync3(packageJsonPath, "utf-8");
539
660
  const parsed = JSON.parse(content);
540
661
  const existingDevDeps = parsed["devDependencies"] ?? {};
541
662
  parsed["devDependencies"] = { ...SHARED_DEV_DEPS, ...existingDevDeps };
542
663
  const existingScripts = parsed["scripts"] ?? {};
543
664
  parsed["scripts"] = { ...SHARED_SCRIPTS, ...existingScripts };
544
- writeFileSync(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
665
+ writeFileSync2(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
545
666
  `, "utf-8");
546
- return Result.ok(undefined);
667
+ return Result2.ok(undefined);
547
668
  } catch (error) {
548
669
  const message = error instanceof Error ? error.message : "Unknown error";
549
- return Result.err(new InitError(`Failed to inject shared config: ${message}`));
670
+ return Result2.err(new InitError(`Failed to inject shared config: ${message}`));
550
671
  }
551
672
  }
552
673
  async function runInit(options) {
553
674
  const { targetDir, force } = options;
554
- const resolvedTargetDir = resolve2(targetDir);
555
- const templateNameResult = await resolveTemplateName(options);
556
- if (templateNameResult.isErr()) {
557
- return templateNameResult;
675
+ const resolvedTargetDir = resolve3(targetDir);
676
+ if (hasPackageJson(resolvedTargetDir) && !force) {
677
+ return Result2.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.`));
558
678
  }
559
- const templateName = templateNameResult.value;
679
+ const templateName = resolveTemplateName(options);
560
680
  const templateResult = validateTemplate(templateName);
561
681
  if (templateResult.isErr()) {
562
682
  return templateResult;
563
683
  }
564
684
  const templatePath = templateResult.value;
565
- const packageInfo = readPackageInfo(resolvedTargetDir);
566
- const packageNameResult = await resolvePackageName(options, resolvedTargetDir, packageInfo);
567
- if (packageNameResult.isErr()) {
568
- return packageNameResult;
569
- }
570
- const packageName = packageNameResult.value;
685
+ const packageName = resolvePackageName(options, resolvedTargetDir);
571
686
  const projectName = deriveProjectName(packageName);
572
- const binNameResult = await resolveBinName(options, projectName, packageInfo);
573
- if (binNameResult.isErr()) {
574
- return binNameResult;
575
- }
576
- const binName = binNameResult.value;
687
+ const binName = resolveBinName(options, projectName);
577
688
  const author = resolveAuthor();
578
689
  const year = resolveYear();
579
690
  const values = {
@@ -586,33 +697,17 @@ async function runInit(options) {
586
697
  author,
587
698
  year
588
699
  };
589
- if (existsSync2(resolvedTargetDir) && !force) {
590
- try {
591
- const entries = readdirSync(resolvedTargetDir);
592
- const significantEntries = entries.filter((e) => !e.startsWith(".") || e === ".gitignore");
593
- if (significantEntries.length > 0) {
594
- for (const entry of significantEntries) {
595
- const templateEntry = `${entry}.template`;
596
- const templateFilePath = join2(templatePath, templateEntry);
597
- const plainFilePath = join2(templatePath, entry);
598
- if (existsSync2(templateFilePath) || existsSync2(plainFilePath)) {
599
- return Result.err(new InitError(`Directory '${resolvedTargetDir}' already exists with files that would be overwritten. Use --force to overwrite.`));
600
- }
601
- }
602
- }
603
- } catch {}
604
- }
605
700
  try {
606
- if (!existsSync2(resolvedTargetDir)) {
607
- mkdirSync(resolvedTargetDir, { recursive: true });
701
+ if (!existsSync3(resolvedTargetDir)) {
702
+ mkdirSync2(resolvedTargetDir, { recursive: true });
608
703
  }
609
704
  } catch (error) {
610
705
  const message = error instanceof Error ? error.message : "Unknown error";
611
- return Result.err(new InitError(`Failed to create target directory: ${message}`));
706
+ return Result2.err(new InitError(`Failed to create target directory: ${message}`));
612
707
  }
613
708
  const templatesDir = getTemplatesDir();
614
- const basePath = join2(templatesDir, "_base");
615
- if (existsSync2(basePath)) {
709
+ const basePath = join3(templatesDir, "_base");
710
+ if (existsSync3(basePath)) {
616
711
  const baseResult = copyTemplateFiles(basePath, resolvedTargetDir, values, force);
617
712
  if (baseResult.isErr()) {
618
713
  return baseResult;
@@ -632,7 +727,36 @@ async function runInit(options) {
632
727
  return rewriteResult;
633
728
  }
634
729
  }
635
- return Result.ok(undefined);
730
+ const blocks = resolveBlocks(options);
731
+ let blocksAdded;
732
+ if (blocks && blocks.length > 0) {
733
+ const mergedResult = {
734
+ created: [],
735
+ skipped: [],
736
+ overwritten: [],
737
+ dependencies: {},
738
+ devDependencies: {}
739
+ };
740
+ for (const blockName of blocks) {
741
+ const addResult = await runAdd({
742
+ block: blockName,
743
+ force,
744
+ dryRun: false,
745
+ cwd: resolvedTargetDir
746
+ });
747
+ if (addResult.isErr()) {
748
+ return Result2.err(new InitError(`Failed to add block '${blockName}': ${addResult.error.message}`));
749
+ }
750
+ const blockResult = addResult.value;
751
+ mergedResult.created.push(...blockResult.created);
752
+ mergedResult.skipped.push(...blockResult.skipped);
753
+ mergedResult.overwritten.push(...blockResult.overwritten);
754
+ Object.assign(mergedResult.dependencies, blockResult.dependencies);
755
+ Object.assign(mergedResult.devDependencies, blockResult.devDependencies);
756
+ }
757
+ blocksAdded = mergedResult;
758
+ }
759
+ return Result2.ok({ blocksAdded });
636
760
  }
637
761
  function initCommand(program) {
638
762
  const init = program.command("init").description("Scaffold a new Outfitter project");
@@ -643,7 +767,42 @@ function initCommand(program) {
643
767
  return typeof flags.opts === "function" ? flags.opts() : flags;
644
768
  };
645
769
  const resolveLocal = (flags) => Boolean(flags.local || flags.workspace);
646
- 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);
770
+ 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");
771
+ const printInitResult = (targetDir, result) => {
772
+ console.log(`Project initialized successfully in ${resolve3(targetDir)}`);
773
+ if (result.blocksAdded) {
774
+ const { created, skipped, dependencies, devDependencies } = result.blocksAdded;
775
+ if (created.length > 0) {
776
+ console.log(`
777
+ Added ${created.length} tooling file(s):`);
778
+ for (const file of created) {
779
+ console.log(` ✓ ${file}`);
780
+ }
781
+ }
782
+ if (skipped.length > 0) {
783
+ console.log(`
784
+ Skipped ${skipped.length} existing file(s):`);
785
+ for (const file of skipped) {
786
+ console.log(` - ${file}`);
787
+ }
788
+ }
789
+ const depCount = Object.keys(dependencies).length + Object.keys(devDependencies).length;
790
+ if (depCount > 0) {
791
+ console.log(`
792
+ Added ${depCount} package(s) to package.json:`);
793
+ for (const [name, version] of Object.entries(dependencies)) {
794
+ console.log(` + ${name}@${version}`);
795
+ }
796
+ for (const [name, version] of Object.entries(devDependencies)) {
797
+ console.log(` + ${name}@${version} (dev)`);
798
+ }
799
+ }
800
+ }
801
+ console.log(`
802
+ Next steps:`);
803
+ console.log(" bun install");
804
+ console.log(" bun run dev");
805
+ };
647
806
  withCommonOptions(init.argument("[directory]").option("-t, --template <template>", "Template to use")).action(async (directory, flags, command) => {
648
807
  const targetDir = directory ?? process.cwd();
649
808
  const resolvedFlags = resolveFlags(flags, command);
@@ -654,13 +813,15 @@ function initCommand(program) {
654
813
  template: resolvedFlags.template,
655
814
  local,
656
815
  force: resolvedFlags.force ?? false,
816
+ with: resolvedFlags.with,
817
+ noTooling: resolvedFlags.noTooling,
657
818
  ...resolvedFlags.bin !== undefined ? { bin: resolvedFlags.bin } : {}
658
819
  });
659
820
  if (result.isErr()) {
660
821
  console.error(`Error: ${result.error.message}`);
661
822
  process.exit(1);
662
823
  }
663
- console.log(`Project initialized successfully in ${resolve2(targetDir)}`);
824
+ printInitResult(targetDir, result.value);
664
825
  });
665
826
  withCommonOptions(init.command("cli [directory]").description("Scaffold a new CLI project")).action(async (directory, flags, command) => {
666
827
  const targetDir = directory ?? process.cwd();
@@ -672,13 +833,15 @@ function initCommand(program) {
672
833
  template: "cli",
673
834
  local,
674
835
  force: resolvedFlags.force ?? false,
836
+ with: resolvedFlags.with,
837
+ noTooling: resolvedFlags.noTooling,
675
838
  ...resolvedFlags.bin !== undefined ? { bin: resolvedFlags.bin } : {}
676
839
  });
677
840
  if (result.isErr()) {
678
841
  console.error(`Error: ${result.error.message}`);
679
842
  process.exit(1);
680
843
  }
681
- console.log(`Project initialized successfully in ${resolve2(targetDir)}`);
844
+ printInitResult(targetDir, result.value);
682
845
  });
683
846
  withCommonOptions(init.command("mcp [directory]").description("Scaffold a new MCP server")).action(async (directory, flags, command) => {
684
847
  const targetDir = directory ?? process.cwd();
@@ -690,13 +853,15 @@ function initCommand(program) {
690
853
  template: "mcp",
691
854
  local,
692
855
  force: resolvedFlags.force ?? false,
856
+ with: resolvedFlags.with,
857
+ noTooling: resolvedFlags.noTooling,
693
858
  ...resolvedFlags.bin !== undefined ? { bin: resolvedFlags.bin } : {}
694
859
  });
695
860
  if (result.isErr()) {
696
861
  console.error(`Error: ${result.error.message}`);
697
862
  process.exit(1);
698
863
  }
699
- console.log(`Project initialized successfully in ${resolve2(targetDir)}`);
864
+ printInitResult(targetDir, result.value);
700
865
  });
701
866
  withCommonOptions(init.command("daemon [directory]").description("Scaffold a new daemon project")).action(async (directory, flags, command) => {
702
867
  const targetDir = directory ?? process.cwd();
@@ -708,31 +873,33 @@ function initCommand(program) {
708
873
  template: "daemon",
709
874
  local,
710
875
  force: resolvedFlags.force ?? false,
876
+ with: resolvedFlags.with,
877
+ noTooling: resolvedFlags.noTooling,
711
878
  ...resolvedFlags.bin !== undefined ? { bin: resolvedFlags.bin } : {}
712
879
  });
713
880
  if (result.isErr()) {
714
881
  console.error(`Error: ${result.error.message}`);
715
882
  process.exit(1);
716
883
  }
717
- console.log(`Project initialized successfully in ${resolve2(targetDir)}`);
884
+ printInitResult(targetDir, result.value);
718
885
  });
719
886
  }
720
887
 
721
888
  // src/actions.ts
722
- import { resolve as resolve3 } from "node:path";
889
+ import { resolve as resolve4 } from "node:path";
723
890
  import {
724
891
  createActionRegistry,
725
892
  defineAction,
726
893
  InternalError,
727
- Result as Result2
894
+ Result as Result3
728
895
  } from "@outfitter/contracts";
729
896
  import { z } from "zod";
730
897
 
731
898
  // src/commands/demo.ts
732
- import { isCancel as isCancel2, select as select2 } from "@clack/prompts";
899
+ import { isCancel, select } from "@clack/prompts";
733
900
  import { createTheme as createTheme3, renderTable as renderTable2, SPINNERS } from "@outfitter/cli/render";
734
901
  import { ANSI } from "@outfitter/cli/streaming";
735
- import { isInteractive as isInteractive2 } from "@outfitter/cli/terminal";
902
+ import { isInteractive } from "@outfitter/cli/terminal";
736
903
 
737
904
  // src/commands/demo/index.ts
738
905
  import {
@@ -951,7 +1118,7 @@ async function runDemo(options) {
951
1118
  if (options.section) {
952
1119
  return runSectionByName(options.section);
953
1120
  }
954
- if (isInteractive2()) {
1121
+ if (isInteractive()) {
955
1122
  const selectedSection = await selectSection();
956
1123
  if (selectedSection === null) {
957
1124
  return { output: "", exitCode: 130 };
@@ -985,11 +1152,11 @@ async function selectSection() {
985
1152
  hint: s.description
986
1153
  }))
987
1154
  ];
988
- const selection = await select2({
1155
+ const selection = await select({
989
1156
  message: "Select a demo section",
990
1157
  options
991
1158
  });
992
- if (isCancel2(selection)) {
1159
+ if (isCancel(selection)) {
993
1160
  return null;
994
1161
  }
995
1162
  return String(selection);
@@ -1165,13 +1332,13 @@ function createInitAction(options) {
1165
1332
  handler: async (input) => {
1166
1333
  const result = await runInit(input);
1167
1334
  if (result.isErr()) {
1168
- return Result2.err(new InternalError({
1335
+ return Result3.err(new InternalError({
1169
1336
  message: result.error.message,
1170
1337
  context: { action: options.id }
1171
1338
  }));
1172
1339
  }
1173
- console.log(`Project initialized successfully in ${resolve3(input.targetDir)}`);
1174
- return Result2.ok({ ok: true });
1340
+ console.log(`Project initialized successfully in ${resolve4(input.targetDir)}`);
1341
+ return Result3.ok({ ok: true });
1175
1342
  }
1176
1343
  });
1177
1344
  }
@@ -1212,7 +1379,7 @@ var demoAction = defineAction({
1212
1379
  if (result.exitCode !== 0) {
1213
1380
  process.exit(result.exitCode);
1214
1381
  }
1215
- return Result2.ok(result);
1382
+ return Result3.ok(result);
1216
1383
  }
1217
1384
  });
1218
1385
  var doctorAction = defineAction({
@@ -1231,7 +1398,79 @@ var doctorAction = defineAction({
1231
1398
  if (result.exitCode !== 0) {
1232
1399
  process.exit(result.exitCode);
1233
1400
  }
1234
- return Result2.ok(result);
1401
+ return Result3.ok(result);
1402
+ }
1403
+ });
1404
+ var addInputSchema = z.object({
1405
+ block: z.string(),
1406
+ force: z.boolean(),
1407
+ dryRun: z.boolean(),
1408
+ cwd: z.string().optional()
1409
+ });
1410
+ var addAction = defineAction({
1411
+ id: "add",
1412
+ description: "Add a block from the registry to your project",
1413
+ surfaces: ["cli"],
1414
+ input: addInputSchema,
1415
+ cli: {
1416
+ group: "add",
1417
+ command: "<block>",
1418
+ description: "Add a block from the registry (claude, biome, lefthook, bootstrap, scaffolding)",
1419
+ options: [
1420
+ {
1421
+ flags: "-f, --force",
1422
+ description: "Overwrite existing files",
1423
+ defaultValue: false
1424
+ },
1425
+ {
1426
+ flags: "--dry-run",
1427
+ description: "Show what would be added without making changes",
1428
+ defaultValue: false
1429
+ }
1430
+ ],
1431
+ mapInput: (context) => ({
1432
+ block: context.args[0],
1433
+ force: Boolean(context.flags["force"]),
1434
+ dryRun: Boolean(context.flags["dry-run"] ?? context.flags["dryRun"]),
1435
+ cwd: process.cwd()
1436
+ })
1437
+ },
1438
+ handler: async (input) => {
1439
+ const result = await runAdd(input);
1440
+ if (result.isErr()) {
1441
+ return Result3.err(new InternalError({
1442
+ message: result.error.message,
1443
+ context: { action: "add" }
1444
+ }));
1445
+ }
1446
+ printAddResults(result.value, input.dryRun);
1447
+ return Result3.ok(result.value);
1448
+ }
1449
+ });
1450
+ var listBlocksAction = defineAction({
1451
+ id: "add.list",
1452
+ description: "List available blocks",
1453
+ surfaces: ["cli"],
1454
+ input: z.object({}),
1455
+ cli: {
1456
+ group: "add",
1457
+ command: "list",
1458
+ description: "List available blocks",
1459
+ mapInput: () => ({})
1460
+ },
1461
+ handler: () => {
1462
+ const result = listBlocks();
1463
+ if (result.isErr()) {
1464
+ return Result3.err(new InternalError({
1465
+ message: result.error.message,
1466
+ context: { action: "add.list" }
1467
+ }));
1468
+ }
1469
+ console.log("Available blocks:");
1470
+ for (const block of result.value) {
1471
+ console.log(` - ${block}`);
1472
+ }
1473
+ return Result3.ok({ blocks: result.value });
1235
1474
  }
1236
1475
  });
1237
1476
  var outfitterActions = createActionRegistry().add(createInitAction({
@@ -1254,6 +1493,6 @@ var outfitterActions = createActionRegistry().add(createInitAction({
1254
1493
  description: "Scaffold a new daemon project",
1255
1494
  command: "daemon [directory]",
1256
1495
  templateOverride: "daemon"
1257
- })).add(demoAction).add(doctorAction);
1496
+ })).add(demoAction).add(doctorAction).add(addAction).add(listBlocksAction);
1258
1497
 
1259
1498
  export { runDoctor, printDoctorResults, doctorCommand, InitError, runInit, initCommand, outfitterActions };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "outfitter",
3
3
  "description": "Outfitter umbrella CLI for scaffolding and project management",
4
- "version": "0.1.0-rc.2",
4
+ "version": "0.1.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -10,28 +10,28 @@
10
10
  "module": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
12
12
  "exports": {
13
- ".": {
14
- "import": {
15
- "types": "./dist/index.d.ts",
16
- "default": "./dist/index.js"
17
- }
18
- },
19
13
  "./commands/demo/errors": {
20
14
  "import": {
21
15
  "types": "./dist/commands/demo/errors.d.ts",
22
16
  "default": "./dist/commands/demo/errors.js"
23
17
  }
24
18
  },
19
+ "./commands/demo": {
20
+ "import": {
21
+ "types": "./dist/commands/demo.d.ts",
22
+ "default": "./dist/commands/demo.js"
23
+ }
24
+ },
25
25
  "./actions": {
26
26
  "import": {
27
27
  "types": "./dist/actions.d.ts",
28
28
  "default": "./dist/actions.js"
29
29
  }
30
30
  },
31
- "./commands/shared-deps": {
31
+ ".": {
32
32
  "import": {
33
- "types": "./dist/commands/shared-deps.d.ts",
34
- "default": "./dist/commands/shared-deps.js"
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
35
  }
36
36
  },
37
37
  "./commands/doctor": {
@@ -46,10 +46,16 @@
46
46
  "default": "./dist/commands/init.js"
47
47
  }
48
48
  },
49
- "./commands/demo": {
49
+ "./commands/shared-deps": {
50
+ "import": {
51
+ "types": "./dist/commands/shared-deps.d.ts",
52
+ "default": "./dist/commands/shared-deps.js"
53
+ }
54
+ },
55
+ "./commands/add": {
50
56
  "import": {
51
- "types": "./dist/commands/demo/index.d.ts",
52
- "default": "./dist/commands/demo/index.js"
57
+ "types": "./dist/commands/add.d.ts",
58
+ "default": "./dist/commands/add.js"
53
59
  }
54
60
  },
55
61
  "./package.json": "./package.json"
@@ -74,9 +80,10 @@
74
80
  },
75
81
  "dependencies": {
76
82
  "@clack/prompts": "^0.10.0",
77
- "@outfitter/cli": "0.1.0-rc.3",
78
- "@outfitter/config": "0.1.0-rc.3",
79
- "@outfitter/contracts": "0.1.0-rc.2",
83
+ "@outfitter/cli": "0.1.0",
84
+ "@outfitter/config": "0.1.0",
85
+ "@outfitter/contracts": "0.1.0",
86
+ "@outfitter/tooling": "0.1.0",
80
87
  "commander": "^14.0.2",
81
88
  "zod": "^4.3.5"
82
89
  },