@askrjs/cli 0.0.24 → 0.0.25

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/add.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as writeFileChanges } from "./file-changes-cmFN-rF8.js";
1
+ import { n as writeFileChanges } from "./file-changes-Va4jkhMm.js";
2
2
  //#region src/bin/add.d.ts
3
3
  type CliIo = Pick<Console, "error" | "log">;
4
4
  type WriteChanges = typeof writeFileChanges;
package/dist/add.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
3
- import { t as writeFileChanges } from "./file-changes-BAFLhEHZ.js";
3
+ import { t as writeFileChanges } from "./file-changes-CsrYAEYu.js";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  //#region src/bin/add.ts
@@ -365,9 +365,11 @@ async function addPage(parsed, io, writeChanges) {
365
365
  return 1;
366
366
  }
367
367
  const importSpecifier = toImportSpecifier(routesFile, pageFile);
368
+ let routeFileContent = "";
368
369
  let updatedRoutes = "";
369
370
  try {
370
- updatedRoutes = createUpdatedRouteFile(await fs.readFile(routesFile, "utf8"), {
371
+ routeFileContent = await fs.readFile(routesFile, "utf8");
372
+ updatedRoutes = createUpdatedRouteFile(routeFileContent, {
371
373
  componentName,
372
374
  importSpecifier,
373
375
  routePath
@@ -387,7 +389,8 @@ async function addPage(parsed, io, writeChanges) {
387
389
  })
388
390
  }, {
389
391
  filePath: routesFile,
390
- content: updatedRoutes
392
+ content: updatedRoutes,
393
+ expectedContent: routeFileContent
391
394
  }]);
392
395
  } catch (error) {
393
396
  io.error("Failed to write generated page artifacts.");
@@ -437,6 +440,7 @@ async function addAction(parsed, io, writeChanges) {
437
440
  routePath: parsed.routePath,
438
441
  slug
439
442
  });
443
+ const [registryContent, authorizationContent] = await Promise.all([fs.readFile(registryFile, "utf8"), fs.readFile(authorizationFile, "utf8")]);
440
444
  const actions = await discoverDeclaredActions(projectRoot, {
441
445
  filePath: descriptorFile,
442
446
  content: descriptor
@@ -463,11 +467,13 @@ async function addAction(parsed, io, writeChanges) {
463
467
  },
464
468
  {
465
469
  filePath: registryFile,
466
- content: renderServerActionRegistry(actions)
470
+ content: renderServerActionRegistry(actions),
471
+ expectedContent: registryContent
467
472
  },
468
473
  {
469
474
  filePath: authorizationFile,
470
- content: renderAuthorizationRegistry(actions)
475
+ content: renderAuthorizationRegistry(actions),
476
+ expectedContent: authorizationContent
471
477
  }
472
478
  ]);
473
479
  } catch (error) {
@@ -501,7 +507,8 @@ async function addDatabase(parsed, io, writeChanges) {
501
507
  return 1;
502
508
  }
503
509
  try {
504
- const manifest = JSON.parse(await fs.readFile(manifestFile, "utf8"));
510
+ const manifestContent = await fs.readFile(manifestFile, "utf8");
511
+ const manifest = JSON.parse(manifestContent);
505
512
  manifest.dependencies = {
506
513
  ...manifest.dependencies,
507
514
  "@askrjs/orm": "0.0.0",
@@ -511,9 +518,12 @@ async function addDatabase(parsed, io, writeChanges) {
511
518
  } : {}
512
519
  };
513
520
  manifest.dependencies = Object.fromEntries(Object.entries(manifest.dependencies).sort(([left], [right]) => left.localeCompare(right)));
514
- const currentEnvironment = await fs.readFile(environmentFile, "utf8").catch(() => "");
515
- const additions = (parsed.name === "postgres" ? ["DATABASE_URL=postgres://postgres:postgres@localhost:5432/app", "DATABASE_SHADOW_URL=postgres://postgres:postgres@localhost:5432/app_shadow"] : ["DATABASE_PATH=./data/app.sqlite"]).filter((line) => !currentEnvironment.includes(`${line.split("=")[0]}=`));
516
- const environment = `${currentEnvironment}${currentEnvironment && !currentEnvironment.endsWith("\n") ? "\n" : ""}${additions.join("\n")}${additions.length ? "\n" : ""}`;
521
+ const currentEnvironment = await fs.readFile(environmentFile, "utf8").catch((error) => {
522
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
523
+ throw error;
524
+ });
525
+ const additions = (parsed.name === "postgres" ? ["DATABASE_URL=postgres://postgres:postgres@localhost:5432/app", "DATABASE_SHADOW_URL=postgres://postgres:postgres@localhost:5432/app_shadow"] : ["DATABASE_PATH=./data/app.sqlite"]).filter((line) => !currentEnvironment?.includes(`${line.split("=")[0]}=`));
526
+ const environment = `${currentEnvironment ?? ""}${currentEnvironment && !currentEnvironment.endsWith("\n") ? "\n" : ""}${additions.join("\n")}${additions.length ? "\n" : ""}`;
517
527
  const driverImport = parsed.name;
518
528
  const definition = [
519
529
  "import { defineDatabase } from '@askrjs/orm';",
@@ -553,11 +563,13 @@ async function addDatabase(parsed, io, writeChanges) {
553
563
  },
554
564
  {
555
565
  filePath: environmentFile,
556
- content: environment
566
+ content: environment,
567
+ expectedContent: currentEnvironment
557
568
  },
558
569
  {
559
570
  filePath: manifestFile,
560
- content: `${JSON.stringify(manifest, null, 2)}\n`
571
+ content: `${JSON.stringify(manifest, null, 2)}\n`,
572
+ expectedContent: manifestContent
561
573
  }
562
574
  ]);
563
575
  } catch (error) {
package/dist/analyze.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as FileChange } from "./file-changes-cmFN-rF8.js";
1
+ import { t as FileChange } from "./file-changes-Va4jkhMm.js";
2
2
  import ts from "typescript";
3
3
  //#region src/analyze/types.d.ts
4
4
  declare const ANALYZE_SCHEMA_VERSION = 1;
package/dist/analyze.js CHANGED
@@ -71,7 +71,7 @@ async function runAnalyzeCli(args = process.argv.slice(2), io = console, runtime
71
71
  io.log(helpText.trimEnd());
72
72
  return 0;
73
73
  }
74
- const report = await (runtime.analyze ?? (await import("./runner-D2iNzz9g.js")).runAnalysis)({
74
+ const report = await (runtime.analyze ?? (await import("./runner-CtcqO3cF.js")).runAnalysis)({
75
75
  cwd: parsed.cwd,
76
76
  workspacePatterns: parsed.workspacePatterns,
77
77
  check: parsed.check
package/dist/cli.js CHANGED
@@ -101,7 +101,7 @@ async function runCli(args = process.argv.slice(2), io = console) {
101
101
  return runAnalyzeCli(args.slice(1), io);
102
102
  }
103
103
  if (command === "check" || command === "doctor" || command === "repair") {
104
- const { runGuardrailCli } = await import("./guardrails-BVxzrcBX.js");
104
+ const { runGuardrailCli } = await import("./guardrails-uLDOExsl.js");
105
105
  return runGuardrailCli(command, args.slice(1), io);
106
106
  }
107
107
  if (command === "generate") {
@@ -0,0 +1,154 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ //#region src/file-changes.ts
5
+ const LOCK_RETRY_MS = 10;
6
+ const LOCK_TIMEOUT_MS = 1e4;
7
+ const ORPHANED_LOCK_AGE_MS = 3e4;
8
+ function isNodeError(error, code) {
9
+ return error instanceof Error && "code" in error && error.code === code;
10
+ }
11
+ function delay(milliseconds) {
12
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
13
+ }
14
+ async function ownerIsAlive(lockPath) {
15
+ try {
16
+ const owner = JSON.parse(await fs.readFile(path.join(lockPath, "owner.json"), "utf8"));
17
+ if (!Number.isInteger(owner.pid) || owner.pid <= 0) return void 0;
18
+ try {
19
+ process.kill(owner.pid, 0);
20
+ return true;
21
+ } catch (error) {
22
+ if (isNodeError(error, "ESRCH")) return false;
23
+ return true;
24
+ }
25
+ } catch {
26
+ return;
27
+ }
28
+ }
29
+ async function removeOrphanedLock(lockPath) {
30
+ const ownerAlive = await ownerIsAlive(lockPath);
31
+ if (ownerAlive === true) return false;
32
+ if (ownerAlive === void 0) {
33
+ const stat = await fs.stat(lockPath).catch(() => null);
34
+ if (!stat || Date.now() - stat.mtimeMs < ORPHANED_LOCK_AGE_MS) return false;
35
+ }
36
+ await fs.rm(lockPath, {
37
+ recursive: true,
38
+ force: true
39
+ });
40
+ return true;
41
+ }
42
+ async function acquireFileLock(filePath) {
43
+ const lockPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.askr-lock`);
44
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
45
+ while (true) try {
46
+ await fs.mkdir(lockPath);
47
+ await fs.writeFile(path.join(lockPath, "owner.json"), `${JSON.stringify({ pid: process.pid })}\n`, { flag: "wx" });
48
+ return { lockPath };
49
+ } catch (error) {
50
+ if (!isNodeError(error, "EEXIST")) {
51
+ await fs.rm(lockPath, {
52
+ recursive: true,
53
+ force: true
54
+ }).catch(() => void 0);
55
+ throw error;
56
+ }
57
+ if (await removeOrphanedLock(lockPath)) continue;
58
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for file transaction lock: ${filePath}`);
59
+ await delay(LOCK_RETRY_MS);
60
+ }
61
+ }
62
+ async function releaseFileLocks(locks) {
63
+ await Promise.all([...locks].reverse().map((lock) => fs.rm(lock.lockPath, {
64
+ recursive: true,
65
+ force: true
66
+ })));
67
+ }
68
+ async function readCurrentContent(filePath) {
69
+ try {
70
+ return await fs.readFile(filePath, "utf8");
71
+ } catch (error) {
72
+ if (isNodeError(error, "ENOENT")) return null;
73
+ throw error;
74
+ }
75
+ }
76
+ function hasExpectedContent(change) {
77
+ return Object.prototype.hasOwnProperty.call(change, "expectedContent");
78
+ }
79
+ async function remove(paths) {
80
+ await Promise.all(paths.map((filePath) => fs.rm(filePath, { force: true }).catch(() => void 0)));
81
+ }
82
+ async function restore(changes) {
83
+ let complete = true;
84
+ for (const change of [...changes].reverse()) try {
85
+ if (change.original === null) {
86
+ await fs.rm(change.filePath, { force: true });
87
+ continue;
88
+ }
89
+ const rollbackPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-rollback-${randomUUID()}`);
90
+ await fs.writeFile(rollbackPath, change.original, {
91
+ flag: "wx",
92
+ mode: change.mode
93
+ });
94
+ await fs.rename(rollbackPath, change.filePath);
95
+ } catch {
96
+ complete = false;
97
+ }
98
+ return complete;
99
+ }
100
+ async function writeFileChanges(changes, options = {}) {
101
+ const ordered = [...changes].sort((left, right) => left.filePath.localeCompare(right.filePath));
102
+ if (new Set(ordered.map((change) => change.filePath)).size !== ordered.length) throw new Error("File changes contain duplicate target paths.");
103
+ const replace = options.replace ?? fs.rename;
104
+ const guarded = ordered.filter(hasExpectedContent);
105
+ const locks = [];
106
+ try {
107
+ for (const change of guarded) {
108
+ await fs.mkdir(path.dirname(change.filePath), { recursive: true });
109
+ locks.push(await acquireFileLock(change.filePath));
110
+ }
111
+ for (const change of guarded) if (await readCurrentContent(change.filePath) !== change.expectedContent) throw new Error(`File changed before writing: ${change.filePath}`);
112
+ await writeStagedChanges(ordered, replace);
113
+ } finally {
114
+ await releaseFileLocks(locks);
115
+ }
116
+ }
117
+ async function writeStagedChanges(ordered, replace) {
118
+ const staged = [];
119
+ try {
120
+ for (const change of ordered) {
121
+ await fs.mkdir(path.dirname(change.filePath), { recursive: true });
122
+ const stat = await fs.stat(change.filePath).catch(() => null);
123
+ const original = stat ? await fs.readFile(change.filePath) : null;
124
+ const temporaryPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-change-${randomUUID()}`);
125
+ const mode = stat?.mode ?? 420;
126
+ await fs.writeFile(temporaryPath, change.content, {
127
+ flag: "wx",
128
+ mode
129
+ });
130
+ staged.push({
131
+ ...change,
132
+ original,
133
+ mode,
134
+ temporaryPath
135
+ });
136
+ }
137
+ } catch (error) {
138
+ await remove(staged.map((change) => change.temporaryPath));
139
+ throw error;
140
+ }
141
+ const replaced = [];
142
+ try {
143
+ for (const change of staged) {
144
+ await replace(change.temporaryPath, change.filePath);
145
+ replaced.push(change);
146
+ }
147
+ } catch {
148
+ const complete = await restore(replaced);
149
+ await remove(staged.map((change) => change.temporaryPath));
150
+ throw new Error(complete ? "File replacement failed; completed changes were rolled back." : "File replacement failed and rollback was incomplete.");
151
+ }
152
+ }
153
+ //#endregion
154
+ export { writeFileChanges as t };
@@ -2,6 +2,8 @@
2
2
  interface FileChange {
3
3
  readonly filePath: string;
4
4
  readonly content: string;
5
+ /** Content observed while planning a shared-file edit; `null` means the file was absent. */
6
+ readonly expectedContent?: string | null;
5
7
  }
6
8
  interface FileChangeWriterOptions {
7
9
  readonly replace?: (temporaryPath: string, filePath: string) => Promise<void>;
@@ -98,7 +98,7 @@ async function runGuardrailCli(command, args = process.argv.slice(2), io = conso
98
98
  cwd: parsed.cwd,
99
99
  workspacePatterns: parsed.workspacePatterns
100
100
  };
101
- const { runCheck, runDoctor, runRepair } = await import("./runner-N2qHCBar.js");
101
+ const { runCheck, runDoctor, runRepair } = await import("./runner-D_GIsgIj.js");
102
102
  const report = command === "doctor" ? await runDoctor(options, runtime) : command === "repair" ? await runRepair(options) : await runCheck(options, runtime);
103
103
  if (parsed.json) io.log(JSON.stringify(report));
104
104
  else if (report.command === "doctor") printDoctor(report, io);
@@ -1,4 +1,4 @@
1
- import { t as writeFileChanges } from "./file-changes-BAFLhEHZ.js";
1
+ import { t as writeFileChanges } from "./file-changes-CsrYAEYu.js";
2
2
  import { discoverWorkspaceProject } from "./discovery-DUDrZCIC.js";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
@@ -1,6 +1,6 @@
1
1
  import { t as inspectBundledSkills } from "./skills-C72Z3-Cr.js";
2
2
  import { discoverWorkspaceProject } from "./discovery-DUDrZCIC.js";
3
- import { analysisHasBlockingFindings, runAnalysis } from "./runner-D2iNzz9g.js";
3
+ import { analysisHasBlockingFindings, runAnalysis } from "./runner-CtcqO3cF.js";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { spawn } from "node:child_process";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/cli",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "description": "Unified CLI for the Askr platform",
5
5
  "homepage": "https://github.com/askrjs/askr-cli#readme",
6
6
  "bugs": {
@@ -1,66 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { randomUUID } from "node:crypto";
4
- //#region src/file-changes.ts
5
- async function remove(paths) {
6
- await Promise.all(paths.map((filePath) => fs.rm(filePath, { force: true }).catch(() => void 0)));
7
- }
8
- async function restore(changes) {
9
- let complete = true;
10
- for (const change of [...changes].reverse()) try {
11
- if (change.original === null) {
12
- await fs.rm(change.filePath, { force: true });
13
- continue;
14
- }
15
- const rollbackPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-rollback-${randomUUID()}`);
16
- await fs.writeFile(rollbackPath, change.original, {
17
- flag: "wx",
18
- mode: change.mode
19
- });
20
- await fs.rename(rollbackPath, change.filePath);
21
- } catch {
22
- complete = false;
23
- }
24
- return complete;
25
- }
26
- async function writeFileChanges(changes, options = {}) {
27
- const ordered = [...changes].sort((left, right) => left.filePath.localeCompare(right.filePath));
28
- if (new Set(ordered.map((change) => change.filePath)).size !== ordered.length) throw new Error("File changes contain duplicate target paths.");
29
- const replace = options.replace ?? fs.rename;
30
- const staged = [];
31
- try {
32
- for (const change of ordered) {
33
- await fs.mkdir(path.dirname(change.filePath), { recursive: true });
34
- const stat = await fs.stat(change.filePath).catch(() => null);
35
- const original = stat ? await fs.readFile(change.filePath) : null;
36
- const temporaryPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-change-${randomUUID()}`);
37
- const mode = stat?.mode ?? 420;
38
- await fs.writeFile(temporaryPath, change.content, {
39
- flag: "wx",
40
- mode
41
- });
42
- staged.push({
43
- ...change,
44
- original,
45
- mode,
46
- temporaryPath
47
- });
48
- }
49
- } catch (error) {
50
- await remove(staged.map((change) => change.temporaryPath));
51
- throw error;
52
- }
53
- const replaced = [];
54
- try {
55
- for (const change of staged) {
56
- await replace(change.temporaryPath, change.filePath);
57
- replaced.push(change);
58
- }
59
- } catch {
60
- const complete = await restore(replaced);
61
- await remove(staged.map((change) => change.temporaryPath));
62
- throw new Error(complete ? "File replacement failed; completed changes were rolled back." : "File replacement failed and rollback was incomplete.");
63
- }
64
- }
65
- //#endregion
66
- export { writeFileChanges as t };