wormajs 1.0.0-beta.0 → 1.0.0-beta.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.
@@ -243,15 +243,35 @@ function createTable(head) {
243
243
  style: { head: [], border: [] },
244
244
  });
245
245
  }
246
- /** `worma diff` / `worma diff <id>` / `worma diff latest` */
247
- async function actionDiff(id, { list, project }) {
246
+ /** `worma diff` / `worma diff <id>` / `worma diff latest` / `worma diff --remove <id>` */
247
+ async function actionDiff(id, { list, remove, project }) {
248
248
  const projectPath = project
249
249
  ? (node_path_1.default.isAbsolute(project) ? project : node_path_1.default.resolve(process.cwd(), project))
250
250
  : process.cwd();
251
251
  // Mirror `actionGen`: always resolve the cache from CWD so monorepo
252
252
  // sub-packages share one unified cache root.
253
253
  (0, config_1.setGlobalConfig)({ cacheRoot: process.cwd() });
254
- const { countChanges, getChange, listChanges } = await Promise.resolve().then(() => __importStar(require('../functions/changeReport')));
254
+ const { countChanges, getChange, listChanges, removeChange } = await Promise.resolve().then(() => __importStar(require('../functions/changeReport')));
255
+ // `worma diff --remove 0007` (also `worma diff 0007 --remove` / `--remove latest`)
256
+ if (remove) {
257
+ // `--remove` accepts the id itself; fall back to the positional argument so
258
+ // both `--remove 0007` and `0007 --remove` work.
259
+ const targetId = typeof remove === 'string' ? remove : id;
260
+ if (!targetId) {
261
+ console.log(`\n ${theme_1.theme.warning('?')} Nothing to remove: pass a change id, e.g. ${theme_1.theme.label('worma diff --remove 0007')}.\n`);
262
+ process.exitCode = 1;
263
+ return;
264
+ }
265
+ const removedId = await removeChange(projectPath, targetId);
266
+ if (!removedId) {
267
+ console.log(`\n ${theme_1.theme.warning('?')} No change record found for "${targetId}".\n`);
268
+ process.exitCode = 1;
269
+ return;
270
+ }
271
+ console.log(`\n ${theme_1.theme.success('✔')} Removed change record ${theme_1.theme.label(removedId)}.`);
272
+ console.log(` ${theme_1.theme.dim('Run `worma diff` to list the remaining records.')}\n`);
273
+ return;
274
+ }
255
275
  if (!id || list) {
256
276
  const summaries = await listChanges(projectPath);
257
277
  if (summaries.length === 0) {
package/dist/bin/cli.js CHANGED
@@ -27,6 +27,7 @@ program
27
27
  .description('browse recorded API changes')
28
28
  .argument('[id]', 'change id (e.g. 0007) or `latest`; omit to list all')
29
29
  .option('-l, --list', 'list all recorded changes')
30
+ .option('-r, --remove [id]', 'delete a recorded change (e.g. `worma diff --remove 0007`)')
30
31
  .option('-p, --project <path>', 'project directory')
31
32
  .action(actions_1.actionDiff);
32
33
  program.parse(process.argv);
@@ -9,6 +9,7 @@ exports.changesDirPath = changesDirPath;
9
9
  exports.captureChange = captureChange;
10
10
  exports.listChanges = listChanges;
11
11
  exports.getChange = getChange;
12
+ exports.removeChange = removeChange;
12
13
  const promises_1 = __importDefault(require("node:fs/promises"));
13
14
  const node_path_1 = __importDefault(require("node:path"));
14
15
  const config_1 = require("../config");
@@ -218,6 +219,40 @@ async function getChange(projectPath, id) {
218
219
  const record = await readRecord(projectPath, resolvedId);
219
220
  return record ?? undefined;
220
221
  }
222
+ /** Ids are always the zero-padded sequence written by {@link captureChange}. */
223
+ const CHANGE_ID_RE = /^\d+$/;
224
+ /**
225
+ * Delete a single recorded change.
226
+ *
227
+ * @param projectPath absolute path of the project root
228
+ * @param id `"0007"` or the alias `"latest"` (newest record)
229
+ *
230
+ * `index.json#changeSeq` is deliberately left untouched: it only ever allocates
231
+ * new* ids, so keeping it monotonic guarantees the deleted id is never handed
232
+ * out again for a different record.
233
+ *
234
+ * @returns the id that was deleted, or `undefined` when nothing matched
235
+ */
236
+ async function removeChange(projectPath, id) {
237
+ let resolvedId = id;
238
+ if (!id || id === exports.LATEST_CHANGE_ID) {
239
+ const latestId = await resolveLatestId(projectPath);
240
+ if (!latestId)
241
+ return undefined;
242
+ resolvedId = latestId;
243
+ }
244
+ // Guards against deleting anything outside the changes directory
245
+ // (`--remove ../../some-file` would otherwise unlink an arbitrary file).
246
+ if (!CHANGE_ID_RE.test(resolvedId))
247
+ return undefined;
248
+ try {
249
+ await promises_1.default.unlink(recordFile(projectPath, resolvedId));
250
+ return resolvedId;
251
+ }
252
+ catch {
253
+ return undefined;
254
+ }
255
+ }
221
256
  /**
222
257
  * Return the newest change-record id.
223
258
  *
@@ -387,7 +387,20 @@ class TemplateHelper {
387
387
  // --- Phase 1: Per-tag streaming (render → collect → batch write) ---
388
388
  const nonDirTagTpls = tpls.filter(f => !f.insideTagDir && f.templateType === 'tag');
389
389
  const dirTpls = tpls.filter(f => f.insideTagDir);
390
- const effectiveTags = changedTags ? tags.filter(t => changedTags.has(t)) : tags;
390
+ // Incremental rendering normally skips a tag whose hash is unchanged, which
391
+ // assumes its previously generated files are still on disk. If those files
392
+ // were deleted manually while the cache survived, the unchanged tag would
393
+ // never be emitted again. So an unchanged tag is also re-rendered whenever
394
+ // any of its expected artifacts is missing from the output directory.
395
+ let effectiveTags;
396
+ if (changedTags) {
397
+ const unchangedTags = tags.filter(t => !changedTags.has(t));
398
+ const missingTags = await this.collectTagsWithMissingArtifacts(unchangedTags, nonDirTagTpls, dirTpls, tagApisMap, outputDir);
399
+ effectiveTags = tags.filter(t => changedTags.has(t) || missingTags.has(t));
400
+ }
401
+ else {
402
+ effectiveTags = tags;
403
+ }
391
404
  let tagFilesWritten = 0;
392
405
  logger_1.logger.debug('Phase 1: Per-tag streaming', {
393
406
  totalTags: tags.length,
@@ -461,6 +474,45 @@ class TemplateHelper {
461
474
  logger_1.logger.debug('Generation summary', { totalOutputFiles: allFilePaths.length });
462
475
  return { filePaths: allFilePaths };
463
476
  }
477
+ /**
478
+ * Detect tags whose previously generated artifacts are missing from disk.
479
+ *
480
+ * Mirrors the path derivation used by `renderOne` / `expandByApi` to compute
481
+ * the expected output path of every per-tag file without rendering anything,
482
+ * then returns the tags that must be re-rendered because at least one of
483
+ * their files is gone.
484
+ */
485
+ async collectTagsWithMissingArtifacts(candidateTags, nonDirTagTpls, dirTpls, tagApisMap, outputDir) {
486
+ const missingTags = new Set();
487
+ for (const tag of candidateTags) {
488
+ const tagApis = tagApisMap.get(tag);
489
+ const expectedPaths = [];
490
+ // Flat `[tag]`-named templates, e.g. `services/[tag].ts.handlebars`
491
+ for (const tf of nonDirTagTpls) {
492
+ expectedPaths.push(stripExt(normalizeSlashes(tf.relativePath.replace(constant_1.TemplatePlaceholder.TAG, tag))));
493
+ }
494
+ // Tag-dir templates, e.g. `[tag]/index.ts` and `[tag]/[api].ts`
495
+ for (const tf of dirTpls) {
496
+ if (tf.templateType === 'api') {
497
+ for (const api of tagApis?.apis || []) {
498
+ expectedPaths.push(stripExt(normalizeSlashes(tf.relativePath
499
+ .replace(constant_1.TemplatePlaceholder.TAG, tag)
500
+ .replace(constant_1.TemplatePlaceholder.API, api.name))));
501
+ }
502
+ }
503
+ else {
504
+ expectedPaths.push(stripExt(normalizeSlashes(tf.relativePath.replace(constant_1.TemplatePlaceholder.TAG, tag))));
505
+ }
506
+ }
507
+ for (const relPath of expectedPaths) {
508
+ if (!(await (0, utils_1.existsPromise)(node_path_1.default.join(outputDir, relPath)))) {
509
+ missingTags.add(tag);
510
+ break;
511
+ }
512
+ }
513
+ }
514
+ return missingTags;
515
+ }
464
516
  /**
465
517
  * Remove generated artifacts of tags that no longer exist in the spec.
466
518
  *
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.resolveWorkspaces = exports.logger = exports.generate = exports.writeSourceSnapshot = exports.sourceDocumentHash = exports.readSourceSnapshot = exports.diffSourceDocument = exports.diffApis = exports.apiDiffKey = exports.listChanges = exports.LATEST_CHANGE_ID = exports.getChange = exports.countChanges = exports.defineConfig = exports.createConfig = exports.setGlobalConfig = exports.checkUpdates = void 0;
20
+ exports.resolveWorkspaces = exports.logger = exports.generate = exports.writeSourceSnapshot = exports.sourceDocumentHash = exports.readSourceSnapshot = exports.diffSourceDocument = exports.diffApis = exports.apiDiffKey = exports.removeChange = exports.listChanges = exports.LATEST_CHANGE_ID = exports.getChange = exports.countChanges = exports.defineConfig = exports.createConfig = exports.setGlobalConfig = exports.checkUpdates = void 0;
21
21
  var checkUpdates_1 = require("./checkUpdates");
22
22
  Object.defineProperty(exports, "checkUpdates", { enumerable: true, get: function () { return checkUpdates_1.checkUpdates; } });
23
23
  var config_1 = require("./config");
@@ -31,6 +31,7 @@ Object.defineProperty(exports, "countChanges", { enumerable: true, get: function
31
31
  Object.defineProperty(exports, "getChange", { enumerable: true, get: function () { return changeReport_1.getChange; } });
32
32
  Object.defineProperty(exports, "LATEST_CHANGE_ID", { enumerable: true, get: function () { return changeReport_1.LATEST_CHANGE_ID; } });
33
33
  Object.defineProperty(exports, "listChanges", { enumerable: true, get: function () { return changeReport_1.listChanges; } });
34
+ Object.defineProperty(exports, "removeChange", { enumerable: true, get: function () { return changeReport_1.removeChange; } });
34
35
  var diffApis_1 = require("./functions/diffApis");
35
36
  Object.defineProperty(exports, "apiDiffKey", { enumerable: true, get: function () { return diffApis_1.apiDiffKey; } });
36
37
  Object.defineProperty(exports, "diffApis", { enumerable: true, get: function () { return diffApis_1.diffApis; } });
@@ -73,8 +73,15 @@ function aiDoc(config) {
73
73
  });
74
74
  if (agentValue) {
75
75
  const agentsToInstall = resolveInstallAgents(agentValue);
76
- for (const agent of agentsToInstall) {
77
- installSkill(aidocsDir, agent, projectPath);
76
+ if (agentsToInstall.length) {
77
+ for (const agent of agentsToInstall) {
78
+ installSkill(aidocsDir, agent, projectPath);
79
+ }
80
+ // The skill now lives in each agent's own skills directory. Keeping the
81
+ // generated copy under the output directory would store the same files
82
+ // twice, so it is dropped — but only after *every* install succeeded:
83
+ // a failing install throws above and leaves the source in place.
84
+ removeSkillSourceDir(aidocsDir);
78
85
  }
79
86
  }
80
87
  },
@@ -147,6 +154,24 @@ function installSkill(skillPath, agent, projectPath) {
147
154
  throw logger_1.logger.throwError(error);
148
155
  }
149
156
  }
157
+ /**
158
+ * Delete the generated skill directory once it has been installed.
159
+ *
160
+ * `skills add` copies the whole directory into the agent's own skills folder,
161
+ * so the copy under the generator output is pure duplication.
162
+ *
163
+ * A removal failure is reported but never thrown: the install already
164
+ * succeeded, and failing the whole generation over a leftover directory would
165
+ * be worse than keeping it.
166
+ */
167
+ function removeSkillSourceDir(skillPath) {
168
+ try {
169
+ node_fs_1.default.rmSync(skillPath, { recursive: true, force: true });
170
+ }
171
+ catch (error) {
172
+ console.error(`${prefix}Failed to remove the generated skill directory "${skillPath}".`, error?.stack ?? error);
173
+ }
174
+ }
150
175
  /**
151
176
  * Parse a `key=value` configuration file (same format as an environment file).
152
177
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormajs",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.1",
4
4
  "description": "A modern OpenAPI code generator - Generate type-safe API clients from OpenAPI specs",
5
5
  "author": "worma",
6
6
  "license": "MIT",
@@ -658,6 +658,19 @@ export declare function listChanges(projectPath: string): Promise<ChangeSummary[
658
658
  * @param id `"0007"` or the alias `"latest"` (newest record)
659
659
  */
660
660
  export declare function getChange(projectPath: string, id: string): Promise<Change | undefined>;
661
+ /**
662
+ * Delete a single recorded change.
663
+ *
664
+ * @param projectPath absolute path of the project root
665
+ * @param id `"0007"` or the alias `"latest"` (newest record)
666
+ *
667
+ * `index.json#changeSeq` is deliberately left untouched: it only ever allocates
668
+ * new* ids, so keeping it monotonic guarantees the deleted id is never handed
669
+ * out again for a different record.
670
+ *
671
+ * @returns the id that was deleted, or `undefined` when nothing matched
672
+ */
673
+ export declare function removeChange(projectPath: string, id: string): Promise<string | undefined>;
661
674
  /** A newly added or removed API (identified by `method` + `path`). */
662
675
  export interface ApiChange {
663
676
  method: string;