@wneng/create-keel 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/version.ts
4
- var SCAFFOLDER_VERSION = "0.1.2";
4
+ var SCAFFOLDER_VERSION = "0.2.1";
5
5
 
6
6
  // src/schema/options.ts
7
7
  import Ajv from "ajv";
@@ -151,6 +151,189 @@ function formatError(err) {
151
151
  \u5EFA\u8BAE\uFF1A\u8BF7\u5728 issue \u4E2D\u9644\u4E0A\u5B8C\u6574\u4E0A\u4E0B\u6587`;
152
152
  }
153
153
 
154
+ // src/feature/layers.ts
155
+ var FEATURE_LAYERS = ["pm", "arch", "backend", "frontend", "test"];
156
+ var FEATURE_SLUG_PATTERN = PROJECT_NAME_PATTERN;
157
+ var FEATURE_SLUG_PATTERN_SOURCE = "^[a-z0-9][a-z0-9-]{0,62}$";
158
+ function isValidFeatureSlug(value) {
159
+ return FEATURE_SLUG_PATTERN.test(value);
160
+ }
161
+ function isFeatureLayer(value) {
162
+ return FEATURE_LAYERS.includes(value);
163
+ }
164
+ var LAYER_ARTIFACTS = {
165
+ pm: {
166
+ layer: "pm",
167
+ label: "PRD (docs/01-\u80CC\u666F\u4E0E\u9700\u6C42)",
168
+ placeholderTemplate: "pm/prd.md.eta",
169
+ outputPath: (slug) => `docs/01-\u80CC\u666F\u4E0E\u9700\u6C42/prd-${slug}.md`
170
+ },
171
+ arch: {
172
+ layer: "arch",
173
+ label: "ADR (docs/02-\u7CFB\u7EDF\u65B9\u6848\u4E0E\u67B6\u6784)",
174
+ placeholderTemplate: "arch/adr.md.eta",
175
+ // ADR file names also embed a 4-digit sequence number; the planner
176
+ // resolves that prefix at scaffold time and prepends it. The slug-only
177
+ // suffix is what this function returns; callers that need the full
178
+ // path must compose the prefix.
179
+ outputPath: (slug) => `docs/02-\u7CFB\u7EDF\u65B9\u6848\u4E0E\u67B6\u6784/adr-${slug}.md`
180
+ },
181
+ backend: {
182
+ layer: "backend",
183
+ label: "backend design (docs/04-\u540E\u7AEF\u8BE6\u7EC6\u8BBE\u8BA1)",
184
+ placeholderTemplate: "backend/design.md.eta",
185
+ outputPath: (slug) => `docs/04-\u540E\u7AEF\u8BE6\u7EC6\u8BBE\u8BA1/${slug}.md`
186
+ },
187
+ frontend: {
188
+ layer: "frontend",
189
+ label: "frontend design (docs/05-\u524D\u7AEF\u5BA2\u6237\u7AEF\u8BE6\u7EC6\u8BBE\u8BA1)",
190
+ placeholderTemplate: "frontend/design.md.eta",
191
+ outputPath: (slug) => `docs/05-\u524D\u7AEF\u5BA2\u6237\u7AEF\u8BE6\u7EC6\u8BBE\u8BA1/${slug}-web.md`
192
+ },
193
+ test: {
194
+ layer: "test",
195
+ label: "test plan (docs/07-\u8D28\u91CF\u4E0E\u6D4B\u8BD5/test-plans)",
196
+ placeholderTemplate: "test/test-plan.md.eta",
197
+ outputPath: (slug) => `docs/07-\u8D28\u91CF\u4E0E\u6D4B\u8BD5/test-plans/${slug}.md`
198
+ }
199
+ };
200
+ var LAYER_REQUIRED_ROLE = {
201
+ test: "qa"
202
+ };
203
+ function layerEnabledByRoles(layer, roles) {
204
+ const required = LAYER_REQUIRED_ROLE[layer];
205
+ if (required === void 0) return true;
206
+ return roles.includes(required);
207
+ }
208
+ function resolveLayers(requested, roles) {
209
+ const requestedSet = requested === void 0 || requested === "all" ? new Set(FEATURE_LAYERS) : new Set(requested);
210
+ const out = [];
211
+ for (const layer of FEATURE_LAYERS) {
212
+ if (!requestedSet.has(layer)) continue;
213
+ if (!layerEnabledByRoles(layer, roles)) continue;
214
+ out.push(layer);
215
+ }
216
+ return out;
217
+ }
218
+ function layersDroppedByRoles(requested, roles) {
219
+ const requestedSet = requested === void 0 || requested === "all" ? new Set(FEATURE_LAYERS) : new Set(requested);
220
+ const out = [];
221
+ for (const layer of FEATURE_LAYERS) {
222
+ if (!requestedSet.has(layer)) continue;
223
+ if (layerEnabledByRoles(layer, roles)) continue;
224
+ out.push(layer);
225
+ }
226
+ return out;
227
+ }
228
+
229
+ // src/feature/subcommand.ts
230
+ var ACCEPTED_LAYER_VALUES = [...FEATURE_LAYERS, "all"];
231
+ function parseFeatureAdd(args) {
232
+ let slug;
233
+ let layer;
234
+ let force = false;
235
+ let quiet = false;
236
+ let verbose = false;
237
+ for (let i = 0; i < args.length; i += 1) {
238
+ const a = args[i];
239
+ switch (a) {
240
+ case "--force":
241
+ force = true;
242
+ break;
243
+ case "--quiet":
244
+ quiet = true;
245
+ break;
246
+ case "--verbose":
247
+ verbose = true;
248
+ break;
249
+ case "--layer": {
250
+ const next = args[i + 1];
251
+ if (next === void 0 || next.startsWith("--")) {
252
+ throw new UserInputError(
253
+ "--layer requires a comma-separated list",
254
+ `\u793A\u4F8B\uFF1A--layer pm,backend,frontend\uFF1B\u53EF\u9009\u503C\uFF1A${ACCEPTED_LAYER_VALUES.join(",")}`
255
+ );
256
+ }
257
+ layer = parseLayerArg(next);
258
+ i += 1;
259
+ break;
260
+ }
261
+ default: {
262
+ if (a.startsWith("--")) {
263
+ throw new UserInputError(
264
+ `unknown option: ${a}`,
265
+ "\u67E5\u770B\u53EF\u7528\u9009\u9879\uFF1Acreate-keel feature add --help"
266
+ );
267
+ }
268
+ if (slug === void 0) {
269
+ slug = a;
270
+ } else {
271
+ throw new UserInputError(
272
+ `unexpected extra argument: ${JSON.stringify(a)}`,
273
+ "feature add \u6BCF\u6B21\u53EA\u80FD\u5904\u7406\u4E00\u4E2A slug"
274
+ );
275
+ }
276
+ }
277
+ }
278
+ }
279
+ if (slug === void 0) {
280
+ throw new UserInputError(
281
+ "missing <slug>",
282
+ "\u8BF7\u63D0\u4F9B feature slug\uFF0C\u4F8B\u5982\uFF1Acreate-keel feature add user-signup"
283
+ );
284
+ }
285
+ if (!isValidFeatureSlug(slug)) {
286
+ throw new UserInputError(
287
+ `invalid feature slug: ${JSON.stringify(slug)}`,
288
+ `slug \u5E94\u5339\u914D ${FEATURE_SLUG_PATTERN_SOURCE}\uFF0C\u4F8B\u5982\uFF1Auser-signup`
289
+ );
290
+ }
291
+ const flags = {
292
+ force,
293
+ quiet,
294
+ verbose,
295
+ ...layer !== void 0 ? { layer } : {}
296
+ };
297
+ return { slug, flags };
298
+ }
299
+ function parseLayerArg(raw) {
300
+ const parts = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
301
+ if (parts.length === 0) {
302
+ throw new UserInputError(
303
+ `invalid --layer value: ${JSON.stringify(raw)}`,
304
+ `\u53EF\u9009\u503C\uFF1A${ACCEPTED_LAYER_VALUES.join(",")}`
305
+ );
306
+ }
307
+ if (parts.includes("all")) {
308
+ if (parts.length > 1) {
309
+ throw new UserInputError(
310
+ `--layer all cannot be combined with other layer names`,
311
+ `\u4F20 'all' \u6216\u4E00\u4E2A\u660E\u786E\u5B50\u96C6\uFF0C\u4E0D\u8981\u6DF7\u7528`
312
+ );
313
+ }
314
+ return "all";
315
+ }
316
+ const seen = /* @__PURE__ */ new Set();
317
+ const out = [];
318
+ for (const token of parts) {
319
+ if (!isFeatureLayer(token)) {
320
+ throw new UserInputError(
321
+ `invalid --layer value: ${JSON.stringify(token)}`,
322
+ `\u53EF\u9009\u503C\uFF1A${ACCEPTED_LAYER_VALUES.join(",")}`
323
+ );
324
+ }
325
+ if (seen.has(token)) {
326
+ throw new UserInputError(
327
+ `duplicate layer: ${token}`,
328
+ "--layer \u4E2D\u6BCF\u4E2A layer \u4EC5\u80FD\u51FA\u73B0\u4E00\u6B21"
329
+ );
330
+ }
331
+ seen.add(token);
332
+ out.push(token);
333
+ }
334
+ return out;
335
+ }
336
+
154
337
  // src/cli.ts
155
338
  async function run(io) {
156
339
  const args = io.argv.slice(2);
@@ -167,12 +350,15 @@ async function run(io) {
167
350
  return 0;
168
351
  }
169
352
  const [command, ...rest] = args;
353
+ if (command === "feature") {
354
+ return await dispatchFeature(io, rest);
355
+ }
170
356
  if (command !== "create") {
171
357
  io.stderr(
172
358
  formatError(
173
359
  new UserInputError(
174
360
  `unknown command: ${JSON.stringify(command)}`,
175
- "\u53EF\u7528\u7684\u547D\u4EE4\uFF1Acreate\u3002\u67E5\u770B\u5E2E\u52A9\uFF1A--help"
361
+ "\u53EF\u7528\u7684\u547D\u4EE4\uFF1Acreate / feature\u3002\u67E5\u770B\u5E2E\u52A9\uFF1A--help"
176
362
  )
177
363
  )
178
364
  );
@@ -200,6 +386,7 @@ function parseCreateArgs(args) {
200
386
  let config;
201
387
  let ci;
202
388
  let roles;
389
+ let sampleFeature = false;
203
390
  for (let i = 0; i < args.length; i += 1) {
204
391
  const a = args[i];
205
392
  switch (a) {
@@ -218,6 +405,9 @@ function parseCreateArgs(args) {
218
405
  case "--verbose":
219
406
  verbose = true;
220
407
  break;
408
+ case "--sample-feature":
409
+ sampleFeature = true;
410
+ break;
221
411
  case "--config": {
222
412
  const next = args[i + 1];
223
413
  if (next === void 0 || next.startsWith("--")) {
@@ -309,18 +499,66 @@ function parseCreateArgs(args) {
309
499
  verbose,
310
500
  ...config !== void 0 ? { config } : {},
311
501
  ...ci !== void 0 ? { ci } : {},
312
- ...roles !== void 0 ? { roles } : {}
502
+ ...roles !== void 0 ? { roles } : {},
503
+ ...sampleFeature ? { sampleFeature: true } : {}
313
504
  };
505
+ if (sampleFeature && dryRun) {
506
+ throw new UserInputError(
507
+ `--sample-feature cannot be combined with --dry-run`,
508
+ "\u8BF7\u53BB\u6389\u4E00\u9879\u540E\u91CD\u8BD5"
509
+ );
510
+ }
314
511
  return { projectName, flags };
315
512
  }
513
+ async function dispatchFeature(io, args) {
514
+ if (args.length === 0 || args.some((a) => a === "-h" || a === "--help")) {
515
+ io.stdout(featureHelpText());
516
+ return 0;
517
+ }
518
+ const [sub, ...rest] = args;
519
+ if (sub !== "add") {
520
+ io.stderr(
521
+ formatError(
522
+ new UserInputError(
523
+ `unknown feature subcommand: ${JSON.stringify(sub)}`,
524
+ "\u5F53\u524D\u53EF\u7528\uFF1Afeature add <slug>"
525
+ )
526
+ )
527
+ );
528
+ return 1;
529
+ }
530
+ if (io.onFeatureAdd === void 0) {
531
+ io.stderr(
532
+ formatError(
533
+ new UserInputError(
534
+ `feature add is not wired in this entry point`,
535
+ "\u8FD9\u662F\u4E00\u4E2A scaffolder \u5185\u90E8\u9519\u8BEF\uFF1AonFeatureAdd \u672A\u6CE8\u5165"
536
+ )
537
+ )
538
+ );
539
+ return 1;
540
+ }
541
+ try {
542
+ const parsed = parseFeatureAdd(rest);
543
+ await io.onFeatureAdd(parsed);
544
+ return 0;
545
+ } catch (err) {
546
+ io.stderr(formatError(err));
547
+ if (err instanceof ScaffolderError) {
548
+ return err.exitCode;
549
+ }
550
+ return 1;
551
+ }
552
+ }
316
553
  function helpText() {
317
554
  return [
318
555
  "create-keel \u2014 scaffold a Contract-First project",
319
556
  "",
320
557
  "USAGE",
321
558
  " npx @wneng/create-keel create <project-name> [options]",
559
+ " npx @wneng/create-keel feature add <slug> [options]",
322
560
  "",
323
- "OPTIONS",
561
+ "OPTIONS (create)",
324
562
  " --yes Skip interactive prompts and use defaults",
325
563
  " --force Overwrite a non-empty target directory after confirmation",
326
564
  " --dry-run Compute the plan without writing any files",
@@ -330,15 +568,42 @@ function helpText() {
330
568
  " --ci <platform> Which CI platform to scaffold (gitee|github, default: gitee)",
331
569
  " --roles <list> Comma-separated role directories to scaffold",
332
570
  " (" + ROLE_KINDS.join("|") + "; default: none)",
571
+ " --sample-feature After create, scaffold the user-signup sample feature",
333
572
  " -h, --help Show this help",
334
573
  " -v, --version Show the scaffolder version",
335
574
  "",
336
- "See .kiro/specs/project-scaffolder/ for the full specification."
575
+ "OPTIONS (feature add)",
576
+ " --layer <list> Comma-separated layers to scaffold",
577
+ " (pm|arch|backend|frontend|test|all; default: all)",
578
+ " --force Overwrite existing Layer_Files for the requested layers",
579
+ " --quiet Only emit errors and the final summary",
580
+ " --verbose Emit per-file render/write logs",
581
+ "",
582
+ "See .kiro/specs/feature-subcommand/ for the full specification."
583
+ ].join("\n");
584
+ }
585
+ function featureHelpText() {
586
+ return [
587
+ "create-keel feature \u2014 manage features inside an existing keel project",
588
+ "",
589
+ "USAGE",
590
+ " npx @wneng/create-keel feature add <slug> [options]",
591
+ "",
592
+ "OPTIONS",
593
+ " --layer <list> Comma-separated layers (pm|arch|backend|frontend|test|all)",
594
+ " --force Overwrite existing Layer_Files",
595
+ " --quiet Errors and summary only",
596
+ " --verbose Per-file render/write logs",
597
+ "",
598
+ "EXAMPLES",
599
+ " feature add user-signup",
600
+ " feature add billing-portal --layer pm,backend",
601
+ " feature add legacy-import --force"
337
602
  ].join("\n");
338
603
  }
339
604
 
340
605
  // src/orchestrator.ts
341
- import * as path5 from "node:path";
606
+ import * as path6 from "node:path";
342
607
 
343
608
  // src/input/defaults.ts
344
609
  var OPTION_DEFAULTS = {
@@ -399,8 +664,8 @@ var validate = ajv2.compile(
399
664
  function formatIssues() {
400
665
  const errs = validate.errors ?? [];
401
666
  return errs.map((e) => {
402
- const path6 = e.instancePath || (e.params && "missingProperty" in e.params ? `/${String(e.params.missingProperty)}` : "<root>");
403
- return `${path6}: ${e.message ?? "validation error"}`;
667
+ const path10 = e.instancePath || (e.params && "missingProperty" in e.params ? `/${String(e.params.missingProperty)}` : "<root>");
668
+ return `${path10}: ${e.message ?? "validation error"}`;
404
669
  }).join("; ");
405
670
  }
406
671
  async function loadConfigFile(filePath) {
@@ -897,9 +1162,21 @@ function buildRenderContext(input) {
897
1162
  options: input.options,
898
1163
  year: String(now.getUTCFullYear()),
899
1164
  generatedAt: now.toISOString(),
900
- scaffolderVersion: input.scaffolderVersion
1165
+ scaffolderVersion: input.scaffolderVersion,
1166
+ ...input.feature !== void 0 ? { feature: input.feature } : {}
901
1167
  };
902
1168
  }
1169
+ function buildFeatureContext(slug, nextAdrNumber) {
1170
+ return {
1171
+ slug,
1172
+ title: slugToTitle(slug),
1173
+ nextAdrNumber,
1174
+ contractAnchor: `contracts/openapi/api.yaml#/paths/~1${slug}`
1175
+ };
1176
+ }
1177
+ function slugToTitle(slug) {
1178
+ return slug.split("-").filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
1179
+ }
903
1180
 
904
1181
  // src/plan/builder.ts
905
1182
  import { promises as fs4 } from "node:fs";
@@ -923,7 +1200,11 @@ function toStrictContext(ctx) {
923
1200
  options: ctx.options,
924
1201
  year: ctx.year,
925
1202
  generatedAt: ctx.generatedAt,
926
- scaffolderVersion: ctx.scaffolderVersion
1203
+ scaffolderVersion: ctx.scaffolderVersion,
1204
+ // `feature` is intentionally only attached when the orchestrator
1205
+ // populated it; non-feature templates that try to read it will hit
1206
+ // the proxy's "undefined template variable" branch.
1207
+ ...ctx.feature !== void 0 ? { feature: ctx.feature } : {}
927
1208
  };
928
1209
  return new Proxy(base, {
929
1210
  get(target, prop, receiver) {
@@ -1072,9 +1353,12 @@ function lowerExt(p) {
1072
1353
  // src/schema/metadata.ts
1073
1354
  import Ajv4 from "ajv";
1074
1355
  import addFormats2 from "ajv-formats";
1356
+ import { promises as fs5 } from "node:fs";
1357
+ import * as path4 from "node:path";
1075
1358
  var METADATA_FILENAME = ".scaffolder.json";
1076
1359
  var SEMVER_PATTERN2 = "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-.]+)?(?:\\+[0-9A-Za-z-.]+)?$";
1077
1360
  var FRAGMENT_NAME_PATTERN2 = "^[a-z0-9][a-z0-9-]{0,62}$";
1361
+ var FEATURE_SLUG_PATTERN2 = "^[a-z0-9][a-z0-9-]{0,62}$";
1078
1362
  var METADATA_SCHEMA = {
1079
1363
  $schema: "http://json-schema.org/draft-07/schema#",
1080
1364
  $id: "https://cgsy.example.com/schemas/scaffolder/metadata.json",
@@ -1097,17 +1381,48 @@ var METADATA_SCHEMA = {
1097
1381
  version: { type: "string", pattern: SEMVER_PATTERN2 }
1098
1382
  }
1099
1383
  }
1384
+ },
1385
+ features: {
1386
+ type: "array",
1387
+ items: { type: "string", pattern: FEATURE_SLUG_PATTERN2 },
1388
+ uniqueItems: true
1100
1389
  }
1101
1390
  }
1102
1391
  };
1103
1392
  var ajv4 = new Ajv4({ allErrors: true, strict: true });
1104
1393
  addFormats2(ajv4);
1105
1394
  var validateFn3 = ajv4.compile(METADATA_SCHEMA);
1395
+ var MetadataValidationError = class extends Error {
1396
+ issues;
1397
+ constructor(issues) {
1398
+ super(
1399
+ `Invalid ScaffolderMetadata:
1400
+ ` + issues.map((i) => ` - ${i.path || "<root>"}: ${i.message}`).join("\n")
1401
+ );
1402
+ this.name = "MetadataValidationError";
1403
+ this.issues = issues;
1404
+ }
1405
+ };
1406
+ function collectIssues3() {
1407
+ const errs = validateFn3.errors ?? [];
1408
+ return errs.map((e) => ({
1409
+ path: e.instancePath || (e.params && "missingProperty" in e.params ? `/${String(e.params.missingProperty)}` : ""),
1410
+ message: e.message ?? "validation error"
1411
+ }));
1412
+ }
1413
+ function assertMetadata(value) {
1414
+ if (!validateFn3(value)) {
1415
+ throw new MetadataValidationError(collectIssues3());
1416
+ }
1417
+ assertOptions(value.options);
1418
+ return value;
1419
+ }
1106
1420
  var METADATA_TOP_ORDER = [
1107
1421
  "scaffolderVersion",
1108
1422
  "generatedAt",
1109
1423
  "options",
1110
- "templateFragments"
1424
+ "templateFragments",
1425
+ "features"
1111
1426
  ];
1112
1427
  function canonicalOptions(options) {
1113
1428
  const out = {};
@@ -1126,6 +1441,11 @@ function canonicalMetadata(metadata) {
1126
1441
  out[key] = canonicalOptions(metadata.options);
1127
1442
  } else if (key === "templateFragments") {
1128
1443
  out[key] = metadata.templateFragments.map(canonicalFragmentRef);
1444
+ } else if (key === "features") {
1445
+ const features = metadata.features;
1446
+ if (features !== void 0 && features.length > 0) {
1447
+ out[key] = [...features];
1448
+ }
1129
1449
  } else {
1130
1450
  out[key] = metadata[key];
1131
1451
  }
@@ -1135,13 +1455,35 @@ function canonicalMetadata(metadata) {
1135
1455
  function serializeMetadata(metadata) {
1136
1456
  return JSON.stringify(canonicalMetadata(metadata), null, 2) + "\n";
1137
1457
  }
1458
+ function parseMetadata(input) {
1459
+ let raw;
1460
+ try {
1461
+ raw = JSON.parse(input);
1462
+ } catch (e) {
1463
+ throw new MetadataValidationError([
1464
+ { path: "", message: `invalid JSON: ${e.message}` }
1465
+ ]);
1466
+ }
1467
+ return assertMetadata(raw);
1468
+ }
1469
+ async function writeMetadataFile(targetDirectory, metadata) {
1470
+ const filePath = path4.join(targetDirectory, METADATA_FILENAME);
1471
+ await fs5.writeFile(filePath, serializeMetadata(metadata), "utf8");
1472
+ return filePath;
1473
+ }
1474
+ async function loadMetadataFile(targetDirectory) {
1475
+ const filePath = path4.join(targetDirectory, METADATA_FILENAME);
1476
+ const text = await fs5.readFile(filePath, "utf8");
1477
+ return parseMetadata(text);
1478
+ }
1138
1479
  function buildMetadata(params) {
1139
1480
  const now = params.now ?? /* @__PURE__ */ new Date();
1140
1481
  return {
1141
1482
  scaffolderVersion: params.scaffolderVersion,
1142
1483
  generatedAt: now.toISOString(),
1143
1484
  options: params.options,
1144
- templateFragments: [...params.templateFragments]
1485
+ templateFragments: [...params.templateFragments],
1486
+ ...params.features !== void 0 ? { features: [...params.features] } : {}
1145
1487
  };
1146
1488
  }
1147
1489
 
@@ -1165,14 +1507,14 @@ function appendScaffolderMetadata(input) {
1165
1507
  }
1166
1508
 
1167
1509
  // src/writer/writer.ts
1168
- import { promises as fs5 } from "node:fs";
1169
- import * as path4 from "node:path";
1510
+ import { promises as fs6 } from "node:fs";
1511
+ import * as path5 from "node:path";
1170
1512
  var FORCE_BACKUP_BYTE_LIMIT = 10 * 1024 * 1024;
1171
1513
  async function writePlan(plan, opts) {
1172
- const absTarget = path4.resolve(opts.targetDirectory);
1514
+ const absTarget = path5.resolve(opts.targetDirectory);
1173
1515
  const journal = { createdFiles: [], createdDirectories: [], overwrites: [] };
1174
1516
  const targetState = await inspectTarget(absTarget);
1175
- if (targetState.exists && targetState.nonEmpty && !opts.force) {
1517
+ if (targetState.exists && targetState.nonEmpty && !opts.force && !opts.allowNonEmptyTarget) {
1176
1518
  throw new UserInputError(
1177
1519
  `target directory is not empty: ${absTarget}`,
1178
1520
  "\u4F7F\u7528\u4E0D\u540C\u8DEF\u5F84\uFF0C\u6216\u52A0 --force\uFF08\u5C0F\u5FC3\uFF1A\u4F1A\u8986\u76D6\u65E2\u6709\u6587\u4EF6\uFF09"
@@ -1183,13 +1525,13 @@ async function writePlan(plan, opts) {
1183
1525
  }
1184
1526
  try {
1185
1527
  if (!targetState.exists) {
1186
- await fs5.mkdir(absTarget, { recursive: true });
1528
+ await fs6.mkdir(absTarget, { recursive: true });
1187
1529
  journal.createdDirectories.push(absTarget);
1188
1530
  }
1189
1531
  for (const rel of plan.directories) {
1190
- const abs = path4.join(absTarget, rel);
1532
+ const abs = path5.join(absTarget, rel);
1191
1533
  try {
1192
- await fs5.mkdir(abs);
1534
+ await fs6.mkdir(abs);
1193
1535
  journal.createdDirectories.push(abs);
1194
1536
  } catch (err) {
1195
1537
  if (err.code === "EEXIST") {
@@ -1199,18 +1541,18 @@ async function writePlan(plan, opts) {
1199
1541
  }
1200
1542
  }
1201
1543
  for (const file of plan.files) {
1202
- const abs = path4.join(absTarget, file.targetPath);
1203
- await fs5.mkdir(path4.dirname(abs), { recursive: true });
1544
+ const abs = path5.join(absTarget, file.targetPath);
1545
+ await fs6.mkdir(path5.dirname(abs), { recursive: true });
1204
1546
  if (opts.force) {
1205
1547
  try {
1206
- const prev = await fs5.readFile(abs, "utf8");
1548
+ const prev = await fs6.readFile(abs, "utf8");
1207
1549
  journal.overwrites.push({ absPath: abs, originalContent: prev });
1208
1550
  } catch {
1209
1551
  }
1210
1552
  }
1211
- await fs5.writeFile(abs, file.content, "utf8");
1553
+ await fs6.writeFile(abs, file.content, "utf8");
1212
1554
  if (file.mode !== void 0) {
1213
- await fs5.chmod(abs, file.mode);
1555
+ await fs6.chmod(abs, file.mode);
1214
1556
  }
1215
1557
  journal.createdFiles.push(abs);
1216
1558
  }
@@ -1230,7 +1572,7 @@ async function writePlan(plan, opts) {
1230
1572
  }
1231
1573
  async function inspectTarget(absTarget) {
1232
1574
  try {
1233
- const entries = await fs5.readdir(absTarget);
1575
+ const entries = await fs6.readdir(absTarget);
1234
1576
  return { exists: true, nonEmpty: entries.length > 0 };
1235
1577
  } catch (err) {
1236
1578
  const code = err.code;
@@ -1243,9 +1585,9 @@ async function inspectTarget(absTarget) {
1243
1585
  }
1244
1586
  async function preflightBackupCapacity(absTarget, plan) {
1245
1587
  for (const file of plan.files) {
1246
- const abs = path4.join(absTarget, file.targetPath);
1588
+ const abs = path5.join(absTarget, file.targetPath);
1247
1589
  try {
1248
- const stat = await fs5.stat(abs);
1590
+ const stat = await fs6.stat(abs);
1249
1591
  if (stat.isFile() && stat.size > FORCE_BACKUP_BYTE_LIMIT) {
1250
1592
  throw new UserInputError(
1251
1593
  `--force would overwrite ${abs} which is larger than the 10 MB rollback limit`,
@@ -1266,7 +1608,7 @@ async function rollback(journal) {
1266
1608
  for (let i = journal.overwrites.length - 1; i >= 0; i -= 1) {
1267
1609
  const { absPath, originalContent } = journal.overwrites[i];
1268
1610
  try {
1269
- await fs5.writeFile(absPath, originalContent, "utf8");
1611
+ await fs6.writeFile(absPath, originalContent, "utf8");
1270
1612
  } catch {
1271
1613
  }
1272
1614
  }
@@ -1275,14 +1617,14 @@ async function rollback(journal) {
1275
1617
  const p = journal.createdFiles[i];
1276
1618
  if (overwriteSet.has(p)) continue;
1277
1619
  try {
1278
- await fs5.rm(p, { force: true });
1620
+ await fs6.rm(p, { force: true });
1279
1621
  } catch {
1280
1622
  }
1281
1623
  }
1282
1624
  for (let i = journal.createdDirectories.length - 1; i >= 0; i -= 1) {
1283
1625
  const dir = journal.createdDirectories[i];
1284
1626
  try {
1285
- await fs5.rmdir(dir);
1627
+ await fs6.rmdir(dir);
1286
1628
  } catch {
1287
1629
  }
1288
1630
  }
@@ -1390,7 +1732,7 @@ async function runCreate(input) {
1390
1732
  validatePlanSyntax(plan);
1391
1733
  reporter.info(`planned ${plan.files.length} file(s) across ${plan.directories.length} directory`);
1392
1734
  const cwd = input.io.cwd ?? process.cwd();
1393
- const targetDirectory = path5.resolve(cwd, options.projectName);
1735
+ const targetDirectory = path6.resolve(cwd, options.projectName);
1394
1736
  if (input.flags.dryRun) {
1395
1737
  reporter.stage("dry-run");
1396
1738
  reporter.dryRunReport(plan);
@@ -1449,9 +1791,409 @@ async function resolveOptions(input) {
1449
1791
  return assertOptions(prompted);
1450
1792
  }
1451
1793
 
1794
+ // src/feature/orchestrator.ts
1795
+ import { promises as fs8 } from "node:fs";
1796
+ import * as path9 from "node:path";
1797
+
1798
+ // src/feature/plan.ts
1799
+ import { promises as fs7, accessSync as accessSync2 } from "node:fs";
1800
+ import * as path7 from "node:path";
1801
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1802
+
1803
+ // src/feature/openapi-edit.ts
1804
+ import { isMap, isScalar, parseDocument, YAMLMap } from "yaml";
1805
+ function addPathStub(input) {
1806
+ const insertedPath = input.path ?? `/${input.slug}`;
1807
+ const doc = parseAndValidate(input.currentText);
1808
+ const pathsNode = doc.get("paths", true);
1809
+ let pathsMap;
1810
+ if (pathsNode === void 0 || pathsNode === null || isScalar(pathsNode) && pathsNode.value === null) {
1811
+ pathsMap = new YAMLMap();
1812
+ pathsMap.flow = false;
1813
+ doc.set("paths", pathsMap);
1814
+ } else if (isMap(pathsNode)) {
1815
+ pathsMap = pathsNode;
1816
+ if (pathsMap.items.length === 0 && pathsMap.flow) {
1817
+ pathsMap.flow = false;
1818
+ }
1819
+ } else {
1820
+ throw new TemplateError(
1821
+ `contracts/openapi/api.yaml: 'paths' is present but is not a map`,
1822
+ "\u8BF7\u628A paths \u5B57\u6BB5\u5199\u6210 YAML \u6620\u5C04\uFF08\u4F8B\u5982 paths: {}\uFF09"
1823
+ );
1824
+ }
1825
+ if (pathsMap.has(insertedPath)) {
1826
+ return {
1827
+ nextText: input.currentText,
1828
+ insertedPath,
1829
+ alreadyExisted: true
1830
+ };
1831
+ }
1832
+ const operationKey = input.operation ?? "get";
1833
+ const responses = input.responses ?? { "200": { description: "OK" } };
1834
+ const operationBody = {
1835
+ summary: input.title,
1836
+ operationId: slugToOperationId(input.slug),
1837
+ ...input.requestBody !== void 0 ? { requestBody: input.requestBody } : {},
1838
+ responses
1839
+ };
1840
+ const stub = doc.createNode({
1841
+ [operationKey]: operationBody
1842
+ });
1843
+ pathsMap.set(insertedPath, stub);
1844
+ const nextText = serializeDocument(doc);
1845
+ return {
1846
+ nextText,
1847
+ insertedPath,
1848
+ alreadyExisted: false
1849
+ };
1850
+ }
1851
+ function parseAndValidate(currentText) {
1852
+ const doc = parseDocument(currentText, {
1853
+ // keep node identity stable across parse → toString
1854
+ keepSourceTokens: true
1855
+ });
1856
+ if (doc.errors.length > 0) {
1857
+ const first = doc.errors[0];
1858
+ throw yamlSyntaxError(first);
1859
+ }
1860
+ if (doc.contents === null) {
1861
+ throw new TemplateError(
1862
+ `contracts/openapi/api.yaml is empty; an OpenAPI 3.x document is required`,
1863
+ "\u8BF7\u4FDD\u7559\u81F3\u5C11 openapi/info/paths \u4E09\u4E2A\u9876\u5C42\u5B57\u6BB5"
1864
+ );
1865
+ }
1866
+ if (!isMap(doc.contents)) {
1867
+ throw new TemplateError(
1868
+ `contracts/openapi/api.yaml root must be a YAML mapping`,
1869
+ "\u8BF7\u786E\u8BA4\u6587\u4EF6\u4EE5 openapi: ... \u5F00\u5934\uFF0C\u6839\u662F\u4E00\u4E2A\u6620\u5C04"
1870
+ );
1871
+ }
1872
+ return doc;
1873
+ }
1874
+ function serializeDocument(doc) {
1875
+ return doc.toString({
1876
+ lineWidth: 0,
1877
+ indent: 2
1878
+ });
1879
+ }
1880
+ function yamlSyntaxError(err) {
1881
+ const linePos = err.linePos;
1882
+ const where = linePos !== void 0 ? `line ${linePos[0].line}, column ${linePos[0].col}` : `offset ${err.pos[0]}`;
1883
+ return new UserInputError(
1884
+ `contracts/openapi/api.yaml: YAML syntax error at ${where}: ${err.message}`,
1885
+ "\u4FEE\u6B63 YAML \u8BED\u6CD5\u540E\u91CD\u8BD5\uFF0C\u5E38\u89C1\u539F\u56E0\uFF1A\u7F29\u8FDB / \u5F15\u53F7 / \u5192\u53F7\u540E\u7A7A\u683C\u7F3A\u5931"
1886
+ );
1887
+ }
1888
+ function slugToOperationId(slug) {
1889
+ return slug.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
1890
+ }
1891
+
1892
+ // src/feature/plan.ts
1893
+ async function buildFeaturePlan(input) {
1894
+ const templatesRoot = input.templatesRoot ?? defaultFeatureTemplatesRoot();
1895
+ const sourceRoot = input.source === "sample" ? path7.join(templatesRoot, "sample", input.slug) : templatesRoot;
1896
+ const nextAdrNumber = await computeAdrNumberForSlug(input.cwd, input.slug);
1897
+ const renderContext = buildRenderContext({
1898
+ options: input.options,
1899
+ scaffolderVersion: input.scaffolderVersion,
1900
+ feature: buildFeatureContext(input.slug, nextAdrNumber),
1901
+ ...input.now !== void 0 ? { now: input.now } : {}
1902
+ });
1903
+ const files = [];
1904
+ const outputs = /* @__PURE__ */ new Map();
1905
+ for (const layer of input.layers) {
1906
+ const artifact = LAYER_ARTIFACTS[layer];
1907
+ const targetPath = computeTargetPath(layer, input.slug, nextAdrNumber);
1908
+ outputs.set(layer, targetPath);
1909
+ const templatePath = path7.join(sourceRoot, artifact.placeholderTemplate);
1910
+ const content = await renderFeatureTemplate(templatePath, renderContext, layer, input.source);
1911
+ files.push({
1912
+ targetPath,
1913
+ content,
1914
+ contributedBy: `feature:${layer}`
1915
+ });
1916
+ }
1917
+ let openapiPathInserted;
1918
+ let openapiAlreadyExisted;
1919
+ if (input.layers.includes("backend") && input.options.contract !== "none") {
1920
+ const apiPath = "contracts/openapi/api.yaml";
1921
+ const apiAbs = path7.join(input.cwd, apiPath);
1922
+ let currentText;
1923
+ try {
1924
+ currentText = await fs7.readFile(apiAbs, "utf8");
1925
+ } catch (e) {
1926
+ throw new TemplateError(
1927
+ `expected ${apiPath} to exist when contract != 'none' but cannot read it: ${e.message}`,
1928
+ "\u8BF7\u786E\u8BA4 contracts/openapi/api.yaml \u5B58\u5728\uFF1B\u6216\u5C06 .scaffolder.json \u4E2D options.contract \u8BBE\u4E3A none"
1929
+ );
1930
+ }
1931
+ const editResult = input.source === "sample" && input.slug === "user-signup" ? addPathStub({
1932
+ currentText,
1933
+ slug: input.slug,
1934
+ title: renderContext.feature.title,
1935
+ // The sample feature owns its own canonical path + schema,
1936
+ // satisfying Req 6.4 (request body schema + at least one
1937
+ // response body schema).
1938
+ path: "/users/signup",
1939
+ operation: "post",
1940
+ requestBody: {
1941
+ required: true,
1942
+ content: {
1943
+ "application/json": {
1944
+ schema: {
1945
+ type: "object",
1946
+ required: ["email", "password"],
1947
+ properties: {
1948
+ email: { type: "string", format: "email" },
1949
+ password: { type: "string", minLength: 8 }
1950
+ }
1951
+ }
1952
+ }
1953
+ }
1954
+ },
1955
+ responses: {
1956
+ "201": {
1957
+ description: "User created in pending state; verification email sent",
1958
+ content: {
1959
+ "application/json": {
1960
+ schema: {
1961
+ type: "object",
1962
+ required: ["id", "email", "state"],
1963
+ properties: {
1964
+ id: { type: "string", format: "uuid" },
1965
+ email: { type: "string", format: "email" },
1966
+ state: { type: "string", enum: ["pending"] }
1967
+ }
1968
+ }
1969
+ }
1970
+ }
1971
+ },
1972
+ "409": {
1973
+ description: "Email already registered (generic, does not leak existence)"
1974
+ },
1975
+ "429": { description: "Rate-limited" }
1976
+ }
1977
+ }) : addPathStub({
1978
+ currentText,
1979
+ slug: input.slug,
1980
+ title: renderContext.feature.title
1981
+ });
1982
+ files.push({
1983
+ targetPath: apiPath,
1984
+ content: editResult.nextText,
1985
+ contributedBy: "feature:openapi"
1986
+ });
1987
+ openapiPathInserted = editResult.insertedPath;
1988
+ openapiAlreadyExisted = editResult.alreadyExisted;
1989
+ }
1990
+ const sortedFiles = [...files].sort(
1991
+ (a, b) => a.targetPath < b.targetPath ? -1 : a.targetPath > b.targetPath ? 1 : 0
1992
+ );
1993
+ const directories = collectDirectories(sortedFiles);
1994
+ return {
1995
+ files: sortedFiles,
1996
+ directories,
1997
+ outputs,
1998
+ ...openapiPathInserted !== void 0 ? { openapiPathInserted } : {},
1999
+ ...openapiAlreadyExisted !== void 0 ? { openapiAlreadyExisted } : {}
2000
+ };
2001
+ }
2002
+ function computeTargetPath(layer, slug, nextAdrNumber) {
2003
+ if (layer === "arch") {
2004
+ return `docs/02-\u7CFB\u7EDF\u65B9\u6848\u4E0E\u67B6\u6784/adr-${nextAdrNumber}-${slug}.md`;
2005
+ }
2006
+ return LAYER_ARTIFACTS[layer].outputPath(slug);
2007
+ }
2008
+ async function renderFeatureTemplate(templatePath, context, layer, source) {
2009
+ try {
2010
+ return await renderFile(templatePath, context);
2011
+ } catch (e) {
2012
+ throw new TemplateError(
2013
+ `feature template missing for layer "${layer}" (${source}): ${templatePath}: ${e.message}`,
2014
+ source === "sample" ? `\u8BF7\u786E\u8BA4 src/feature/templates/sample/<slug>/${LAYER_ARTIFACTS[layer].placeholderTemplate} \u5B58\u5728` : "\u8BF7\u786E\u8BA4 src/feature/templates/${layer}/ \u4E0B\u5B58\u5728 " + LAYER_ARTIFACTS[layer].placeholderTemplate
2015
+ );
2016
+ }
2017
+ }
2018
+ var ADR_FILE_RE = /^adr-(\d{4,})-/;
2019
+ async function computeAdrNumberForSlug(cwd, slug) {
2020
+ const dir = path7.join(cwd, "docs", "02-\u7CFB\u7EDF\u65B9\u6848\u4E0E\u67B6\u6784");
2021
+ let entries;
2022
+ try {
2023
+ entries = await fs7.readdir(dir);
2024
+ } catch {
2025
+ return "0001";
2026
+ }
2027
+ const slugRe = new RegExp(`^adr-(\\d{4,})-${escapeRegex(slug)}\\.md$`);
2028
+ let max = 0;
2029
+ for (const name of entries) {
2030
+ const slugMatch = slugRe.exec(name);
2031
+ if (slugMatch) {
2032
+ return slugMatch[1];
2033
+ }
2034
+ const m = ADR_FILE_RE.exec(name);
2035
+ if (m) {
2036
+ const n = parseInt(m[1], 10);
2037
+ if (!Number.isNaN(n) && n > max) max = n;
2038
+ }
2039
+ }
2040
+ return String(max + 1).padStart(4, "0");
2041
+ }
2042
+ function escapeRegex(s) {
2043
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2044
+ }
2045
+ function collectDirectories(files) {
2046
+ const set = /* @__PURE__ */ new Set();
2047
+ for (const f of files) {
2048
+ let cur = path7.posix.dirname(f.targetPath);
2049
+ while (cur && cur !== "." && cur !== "/") {
2050
+ set.add(cur);
2051
+ cur = path7.posix.dirname(cur);
2052
+ }
2053
+ }
2054
+ return [...set].sort();
2055
+ }
2056
+ function defaultFeatureTemplatesRoot() {
2057
+ const here = path7.dirname(fileURLToPath2(import.meta.url));
2058
+ const pkgRoot = findPackageRoot2(here);
2059
+ return path7.join(pkgRoot, "src", "feature", "templates");
2060
+ }
2061
+ function findPackageRoot2(start) {
2062
+ let dir = start;
2063
+ for (let i = 0; i < 10; i += 1) {
2064
+ const candidate = path7.join(dir, "package.json");
2065
+ try {
2066
+ accessSync2(candidate);
2067
+ return dir;
2068
+ } catch {
2069
+ }
2070
+ const parent = path7.dirname(dir);
2071
+ if (parent === dir) break;
2072
+ dir = parent;
2073
+ }
2074
+ throw new TemplateError(
2075
+ `unable to locate package.json above ${start}`,
2076
+ "\u8BF7\u786E\u8BA4 scaffolder \u5B89\u88C5\u5B8C\u6574\uFF0Csrc/feature/templates/ \u4E0E package.json \u4F4D\u4E8E\u540C\u4E00\u5C42\u7EA7"
2077
+ );
2078
+ }
2079
+
2080
+ // src/feature/manifest-update.ts
2081
+ import * as path8 from "node:path";
2082
+ async function appendFeatureSlug(projectDirectory, slug) {
2083
+ const manifest = await loadMetadataFile(projectDirectory);
2084
+ const next = withFeatureSlug(manifest, slug);
2085
+ const metadataPath = path8.join(projectDirectory, METADATA_FILENAME);
2086
+ if (next === manifest) {
2087
+ return { updated: false, metadataPath };
2088
+ }
2089
+ await writeMetadataFile(projectDirectory, next);
2090
+ return { updated: true, metadataPath };
2091
+ }
2092
+ function withFeatureSlug(manifest, slug) {
2093
+ const existing = manifest.features ?? [];
2094
+ if (existing.includes(slug)) {
2095
+ return manifest;
2096
+ }
2097
+ return {
2098
+ ...manifest,
2099
+ features: [...existing, slug]
2100
+ };
2101
+ }
2102
+
2103
+ // src/feature/orchestrator.ts
2104
+ async function runFeatureAdd(input) {
2105
+ if (!isValidFeatureSlug(input.slug)) {
2106
+ throw new UserInputError(
2107
+ `invalid feature slug: ${JSON.stringify(input.slug)}`,
2108
+ "slug \u5E94\u5339\u914D ^[a-z0-9][a-z0-9-]{0,62}$\uFF0C\u4F8B\u5982\uFF1Auser-signup"
2109
+ );
2110
+ }
2111
+ let manifest;
2112
+ try {
2113
+ manifest = await loadMetadataFile(input.cwd);
2114
+ } catch (e) {
2115
+ const code = e.code;
2116
+ if (code === "ENOENT") {
2117
+ throw new UserInputError(
2118
+ `no .scaffolder.json found at ${path9.join(input.cwd, METADATA_FILENAME)}`,
2119
+ "\u8BF7\u5728 keel \u9879\u76EE\u6839\u76EE\u5F55\u8FD0\u884C\uFF1B\u6216\u5148\u7528 create-keel create \u521B\u5EFA\u9879\u76EE"
2120
+ );
2121
+ }
2122
+ throw e;
2123
+ }
2124
+ const requested = input.layers ?? "all";
2125
+ const resolved = resolveLayers(requested, manifest.options.roles);
2126
+ const droppedByRoles = layersDroppedByRoles(requested, manifest.options.roles);
2127
+ const featurePlan = await buildFeaturePlan({
2128
+ slug: input.slug,
2129
+ layers: resolved,
2130
+ source: input.source,
2131
+ options: manifest.options,
2132
+ cwd: input.cwd,
2133
+ scaffolderVersion: input.scaffolderVersion,
2134
+ ...input.templatesRoot !== void 0 ? { templatesRoot: input.templatesRoot } : {},
2135
+ ...input.now !== void 0 ? { now: input.now } : {}
2136
+ });
2137
+ const { toWrite, skipped } = await splitPlanByExistence(featurePlan, input.cwd, input.force);
2138
+ if (toWrite.files.length > 0) {
2139
+ await writePlan(toWrite, {
2140
+ targetDirectory: input.cwd,
2141
+ force: input.force,
2142
+ allowNonEmptyTarget: true
2143
+ });
2144
+ }
2145
+ const anchorsInserted = [];
2146
+ if (featurePlan.openapiPathInserted !== void 0 && featurePlan.openapiAlreadyExisted === false && toWrite.files.some((f) => f.targetPath === "contracts/openapi/api.yaml")) {
2147
+ anchorsInserted.push({
2148
+ fromFile: "contracts/openapi/api.yaml",
2149
+ toTarget: featurePlan.openapiPathInserted
2150
+ });
2151
+ }
2152
+ const manifestResult = await appendFeatureSlug(input.cwd, input.slug);
2153
+ return {
2154
+ slug: input.slug,
2155
+ created: toWrite.files.map((f) => f.targetPath),
2156
+ skipped,
2157
+ droppedByRoles,
2158
+ anchorsInserted,
2159
+ manifestUpdated: manifestResult.updated,
2160
+ metadataPath: manifestResult.metadataPath
2161
+ };
2162
+ }
2163
+ async function splitPlanByExistence(plan, cwd, force) {
2164
+ if (force) {
2165
+ return {
2166
+ toWrite: { files: plan.files, directories: plan.directories },
2167
+ skipped: []
2168
+ };
2169
+ }
2170
+ const toWrite = [];
2171
+ const skipped = [];
2172
+ for (const file of plan.files) {
2173
+ const exists = await fileExists(path9.join(cwd, file.targetPath));
2174
+ if (exists) {
2175
+ skipped.push(file.targetPath);
2176
+ } else {
2177
+ toWrite.push(file);
2178
+ }
2179
+ }
2180
+ return {
2181
+ toWrite: { files: toWrite, directories: plan.directories },
2182
+ skipped
2183
+ };
2184
+ }
2185
+ async function fileExists(absPath) {
2186
+ try {
2187
+ await fs8.access(absPath);
2188
+ return true;
2189
+ } catch {
2190
+ return false;
2191
+ }
2192
+ }
2193
+
1452
2194
  // src/index.ts
1453
2195
  async function handleCreate(input) {
1454
- await runCreate({
2196
+ const result = await runCreate({
1455
2197
  projectName: input.projectName,
1456
2198
  flags: input.flags,
1457
2199
  io: {
@@ -1464,6 +2206,28 @@ async function handleCreate(input) {
1464
2206
  isTTY: Boolean(process.stdout.isTTY)
1465
2207
  }
1466
2208
  });
2209
+ if (input.flags.sampleFeature === true && !result.dryRun) {
2210
+ await runFeatureAdd({
2211
+ slug: "user-signup",
2212
+ cwd: result.targetDirectory,
2213
+ source: "sample",
2214
+ // Force is safe here: nothing pre-existing in a brand new project,
2215
+ // so the writer's nonEmpty check is the only thing to bypass.
2216
+ force: true,
2217
+ scaffolderVersion: SCAFFOLDER_VERSION
2218
+ });
2219
+ }
2220
+ }
2221
+ async function handleFeatureAdd(input) {
2222
+ const cwd = process.cwd();
2223
+ await runFeatureAdd({
2224
+ slug: input.slug,
2225
+ cwd,
2226
+ ...input.flags.layer !== void 0 ? { layers: input.flags.layer } : {},
2227
+ source: "placeholder",
2228
+ force: input.flags.force,
2229
+ scaffolderVersion: SCAFFOLDER_VERSION
2230
+ });
1467
2231
  }
1468
2232
  var exitCode = await run({
1469
2233
  argv: process.argv,
@@ -1473,7 +2237,8 @@ var exitCode = await run({
1473
2237
  stderr: (line) => {
1474
2238
  console.error(line);
1475
2239
  },
1476
- onCreate: handleCreate
2240
+ onCreate: handleCreate,
2241
+ onFeatureAdd: handleFeatureAdd
1477
2242
  });
1478
2243
  process.exit(exitCode);
1479
2244
  //# sourceMappingURL=index.js.map