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 +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.js +1 -1
- package/dist/shared/{chunk-1rebv144.js → chunk-429agb93.js} +460 -221
- package/package.json +23 -16
package/dist/cli.js
CHANGED
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<
|
|
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
|
@@ -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 {
|
|
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 =
|
|
495
|
+
let currentDir = dirname2(fileURLToPath2(import.meta.url));
|
|
268
496
|
for (let i = 0;i < 10; i++) {
|
|
269
|
-
const templatesPath =
|
|
270
|
-
if (
|
|
497
|
+
const templatesPath = join3(currentDir, "templates");
|
|
498
|
+
if (existsSync3(templatesPath)) {
|
|
271
499
|
return templatesPath;
|
|
272
500
|
}
|
|
273
|
-
currentDir =
|
|
501
|
+
currentDir = dirname2(currentDir);
|
|
274
502
|
}
|
|
275
|
-
return
|
|
503
|
+
return join3(process.cwd(), "templates");
|
|
276
504
|
}
|
|
277
505
|
function validateTemplate(templateName) {
|
|
278
506
|
const templatesDir = getTemplatesDir();
|
|
279
|
-
const templatePath =
|
|
280
|
-
if (!
|
|
281
|
-
return
|
|
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
|
|
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
|
|
300
|
-
return
|
|
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
|
|
348
|
-
|
|
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
|
-
|
|
357
|
-
|
|
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
|
-
|
|
416
|
-
|
|
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 (!
|
|
460
|
-
|
|
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 =
|
|
585
|
+
const sourcePath = join3(templateDir, entry);
|
|
465
586
|
const stat = statSync(sourcePath);
|
|
466
587
|
if (stat.isDirectory()) {
|
|
467
|
-
const targetSubDir =
|
|
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 =
|
|
475
|
-
if (
|
|
476
|
-
return
|
|
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 =
|
|
480
|
-
|
|
600
|
+
const buffer = readFileSync3(sourcePath);
|
|
601
|
+
writeFileSync2(targetPath, buffer);
|
|
481
602
|
} else {
|
|
482
|
-
const content =
|
|
603
|
+
const content = readFileSync3(sourcePath, "utf-8");
|
|
483
604
|
const processedContent = replacePlaceholders(content, values);
|
|
484
|
-
|
|
605
|
+
writeFileSync2(targetPath, processedContent, "utf-8");
|
|
485
606
|
}
|
|
486
607
|
}
|
|
487
608
|
}
|
|
488
|
-
return
|
|
609
|
+
return Result2.ok(undefined);
|
|
489
610
|
} catch (error) {
|
|
490
611
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
491
|
-
return
|
|
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 =
|
|
502
|
-
if (!
|
|
503
|
-
return
|
|
622
|
+
const packageJsonPath = join3(targetDir, "package.json");
|
|
623
|
+
if (!existsSync3(packageJsonPath)) {
|
|
624
|
+
return Result2.ok(undefined);
|
|
504
625
|
}
|
|
505
626
|
try {
|
|
506
|
-
const content =
|
|
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
|
-
|
|
644
|
+
writeFileSync2(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
|
|
524
645
|
`, "utf-8");
|
|
525
646
|
}
|
|
526
|
-
return
|
|
647
|
+
return Result2.ok(undefined);
|
|
527
648
|
} catch (error) {
|
|
528
649
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
529
|
-
return
|
|
650
|
+
return Result2.err(new InitError(`Failed to update local dependencies: ${message}`));
|
|
530
651
|
}
|
|
531
652
|
}
|
|
532
653
|
function injectSharedConfig(targetDir) {
|
|
533
|
-
const packageJsonPath =
|
|
534
|
-
if (!
|
|
535
|
-
return
|
|
654
|
+
const packageJsonPath = join3(targetDir, "package.json");
|
|
655
|
+
if (!existsSync3(packageJsonPath)) {
|
|
656
|
+
return Result2.ok(undefined);
|
|
536
657
|
}
|
|
537
658
|
try {
|
|
538
|
-
const content =
|
|
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
|
-
|
|
665
|
+
writeFileSync2(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
|
|
545
666
|
`, "utf-8");
|
|
546
|
-
return
|
|
667
|
+
return Result2.ok(undefined);
|
|
547
668
|
} catch (error) {
|
|
548
669
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
549
|
-
return
|
|
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 =
|
|
555
|
-
|
|
556
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 (!
|
|
607
|
-
|
|
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
|
|
706
|
+
return Result2.err(new InitError(`Failed to create target directory: ${message}`));
|
|
612
707
|
}
|
|
613
708
|
const templatesDir = getTemplatesDir();
|
|
614
|
-
const basePath =
|
|
615
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
884
|
+
printInitResult(targetDir, result.value);
|
|
718
885
|
});
|
|
719
886
|
}
|
|
720
887
|
|
|
721
888
|
// src/actions.ts
|
|
722
|
-
import { resolve as
|
|
889
|
+
import { resolve as resolve4 } from "node:path";
|
|
723
890
|
import {
|
|
724
891
|
createActionRegistry,
|
|
725
892
|
defineAction,
|
|
726
893
|
InternalError,
|
|
727
|
-
Result as
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
1155
|
+
const selection = await select({
|
|
989
1156
|
message: "Select a demo section",
|
|
990
1157
|
options
|
|
991
1158
|
});
|
|
992
|
-
if (
|
|
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
|
|
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 ${
|
|
1174
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
31
|
+
".": {
|
|
32
32
|
"import": {
|
|
33
|
-
"types": "./dist/
|
|
34
|
-
"default": "./dist/
|
|
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/
|
|
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/
|
|
52
|
-
"default": "./dist/commands/
|
|
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
|
|
78
|
-
"@outfitter/config": "0.1.0
|
|
79
|
-
"@outfitter/contracts": "0.1.0
|
|
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
|
},
|