@indigoai-us/hq-cli 5.272.0 → 5.273.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.
@@ -1,6 +1,5 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { randomUUID } from "node:crypto";
4
3
  import { LanesError } from "./errors.js";
5
4
  const KIND_SET = new Set(["blocker", "kpi", "okr", "directive"]);
6
5
  const ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
@@ -18,25 +17,6 @@ function recordPath(root, kind, id) {
18
17
  throw storeError("tracker_kind_invalid", "Unknown tracker record kind", { kind });
19
18
  return path.join(root, kind, `${id}.json`);
20
19
  }
21
- function assertRealDirectory(directory) {
22
- try {
23
- const stat = fs.lstatSync(directory);
24
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
25
- throw storeError("tracker_store_path_invalid", "Tracker storage path must be a real directory", { path: directory });
26
- }
27
- }
28
- catch (err) {
29
- if (err.code !== "ENOENT")
30
- throw err;
31
- fs.mkdirSync(directory, { mode: 0o700 });
32
- }
33
- }
34
- function ensureKindDirectory(root, kind) {
35
- assertRealDirectory(root);
36
- const directory = path.join(root, kind);
37
- assertRealDirectory(directory);
38
- return directory;
39
- }
40
20
  function existingKindDirectory(root, kind) {
41
21
  try {
42
22
  const rootStat = fs.lstatSync(root);
@@ -86,105 +66,7 @@ function parseRecord(text, expectedKind, id, file) {
86
66
  }
87
67
  return record;
88
68
  }
89
- function writeExclusive(file, contents) {
90
- const dir = path.dirname(file);
91
- const temp = path.join(dir, `.${path.basename(file)}.${randomUUID()}.tmp`);
92
- try {
93
- fs.writeFileSync(temp, contents, { encoding: "utf8", flag: "wx", mode: 0o600 });
94
- // link is an atomic create-if-absent operation; rename would overwrite a
95
- // concurrent creator's record if a generated id ever collided.
96
- fs.linkSync(temp, file);
97
- }
98
- catch (err) {
99
- if (err.code === "EEXIST") {
100
- throw storeError("tracker_record_exists", "Tracker record id already exists", { path: file });
101
- }
102
- throw err;
103
- }
104
- finally {
105
- removeTempFile(temp);
106
- }
107
- }
108
- function removeTempFile(file) {
109
- try {
110
- fs.unlinkSync(file);
111
- }
112
- catch (err) {
113
- if (err.code !== "ENOENT")
114
- throw err;
115
- }
116
- }
117
- function withRecordLock(file, action) {
118
- const lock = `${file}.lock`;
119
- let fd;
120
- for (let attempt = 0; attempt < 2; attempt += 1) {
121
- try {
122
- fd = fs.openSync(lock, "wx", 0o600);
123
- fs.writeSync(fd, `${process.pid}\n`);
124
- break;
125
- }
126
- catch (err) {
127
- if (err.code !== "EEXIST")
128
- throw err;
129
- // Recover a lock only when it is old and its owner is gone. Rechecking
130
- // inode and mtime before unlinking avoids deleting a freshly replaced lock.
131
- let first;
132
- try {
133
- first = fs.lstatSync(lock);
134
- }
135
- catch (statError) {
136
- if (statError.code === "ENOENT")
137
- continue;
138
- throw statError;
139
- }
140
- if (Date.now() - first.mtimeMs < 5 * 60_000) {
141
- throw storeError("tracker_write_busy", "Tracker record is being updated; retry the command", { path: file });
142
- }
143
- let owner = null;
144
- try {
145
- const raw = fs.readFileSync(lock, "utf8").trim();
146
- const parsed = Number(raw);
147
- if (Number.isSafeInteger(parsed) && parsed > 0)
148
- owner = parsed;
149
- }
150
- catch (readError) {
151
- if (readError.code !== "ENOENT")
152
- throw readError;
153
- }
154
- if (owner !== null) {
155
- try {
156
- process.kill(owner, 0);
157
- throw storeError("tracker_write_busy", "Tracker record is being updated; retry the command", { path: file });
158
- }
159
- catch (probeError) {
160
- if (probeError instanceof LanesError)
161
- throw probeError;
162
- if (probeError.code !== "ESRCH")
163
- throw probeError;
164
- }
165
- }
166
- try {
167
- const second = fs.lstatSync(lock);
168
- if (second.ino === first.ino && second.dev === first.dev && second.mtimeMs === first.mtimeMs)
169
- fs.unlinkSync(lock);
170
- }
171
- catch (unlinkError) {
172
- if (unlinkError.code !== "ENOENT")
173
- throw unlinkError;
174
- }
175
- }
176
- }
177
- if (fd === undefined)
178
- throw storeError("tracker_write_busy", "Tracker record is being updated; retry the command", { path: file });
179
- try {
180
- return action();
181
- }
182
- finally {
183
- fs.closeSync(fd);
184
- fs.unlinkSync(lock);
185
- }
186
- }
187
- export function createLocalTrackerStore(projectDirectory, now = () => new Date()) {
69
+ export function createLocalTrackerStore(projectDirectory) {
188
70
  const root = path.join(projectDirectory, "tracker");
189
71
  const read = (kind, id) => {
190
72
  const file = recordPath(root, kind, id);
@@ -219,59 +101,6 @@ export function createLocalTrackerStore(projectDirectory, now = () => new Date()
219
101
  .filter((record) => record !== null)
220
102
  .sort((a, b) => b.updated_at.localeCompare(a.updated_at) || a.id.localeCompare(b.id));
221
103
  },
222
- create(input) {
223
- const kind = input.kind;
224
- const directory = ensureKindDirectory(root, kind);
225
- const id = typeof input.id === "string" && input.id ? input.id : randomUUID();
226
- const time = now().toISOString();
227
- const record = {
228
- ...input,
229
- schema_version: "hq-lanes-tracker-record.v1",
230
- kind,
231
- id,
232
- company: input.company,
233
- project_id: input.project_id,
234
- version: 1,
235
- created_at: time,
236
- updated_at: time,
237
- };
238
- writeExclusive(path.join(directory, `${id}.json`), `${JSON.stringify(record, null, 2)}\n`);
239
- return record;
240
- },
241
- update(kind, id, expectedVersion, change) {
242
- const file = recordPath(root, kind, id);
243
- const directory = ensureKindDirectory(root, kind);
244
- return withRecordLock(file, () => {
245
- const current = read(kind, id);
246
- if (!current)
247
- throw storeError("tracker_record_not_found", `Tracker ${kind} record ${id} was not found`, { kind, id });
248
- if (current.version !== expectedVersion) {
249
- throw storeError("tracker_version_conflict", `Tracker record version conflict: expected ${expectedVersion}, found ${current.version}`, {
250
- kind, id, expected: expectedVersion, actual: current.version,
251
- });
252
- }
253
- const changed = change(current);
254
- if (changed.id !== current.id || changed.kind !== current.kind || changed.company !== current.company || changed.project_id !== current.project_id || changed.created_at !== current.created_at) {
255
- throw storeError("tracker_update_invalid", "Tracker update cannot change record identity", { kind, id });
256
- }
257
- const next = {
258
- ...changed,
259
- schema_version: "hq-lanes-tracker-record.v1",
260
- version: current.version + 1,
261
- created_at: current.created_at,
262
- updated_at: now().toISOString(),
263
- };
264
- const temp = path.join(directory, `.${id}.${randomUUID()}.tmp`);
265
- try {
266
- fs.writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
267
- fs.renameSync(temp, file);
268
- }
269
- finally {
270
- removeTempFile(temp);
271
- }
272
- return next;
273
- });
274
- },
275
104
  };
276
105
  }
277
106
  //# sourceMappingURL=tracker-store.js.map
@@ -0,0 +1,9 @@
1
+ export declare const SCAFFOLD_CLI_UPGRADE_COMMAND = "npm install -g @indigoai-us/hq-cli@latest";
2
+ export type ScaffoldCliFloorComparison = {
3
+ status: "pass" | "fail" | "warn" | "skip";
4
+ message: string;
5
+ remediation?: string;
6
+ };
7
+ /** Compare an installed CLI version with the scaffold's supported floor. */
8
+ export declare function compareScaffoldCliFloor(required: unknown, currentVersion: string): ScaffoldCliFloorComparison;
9
+ //# sourceMappingURL=scaffold-cli-floor.d.ts.map
@@ -0,0 +1,50 @@
1
+ import semver from "semver";
2
+ export const SCAFFOLD_CLI_UPGRADE_COMMAND = "npm install -g @indigoai-us/hq-cli@latest";
3
+ const MINIMUM_VERSION_RANGE = /^>=(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
4
+ /** Compare an installed CLI version with the scaffold's supported floor. */
5
+ export function compareScaffoldCliFloor(required, currentVersion) {
6
+ if (required === undefined) {
7
+ return {
8
+ status: "skip",
9
+ message: "No requiresHqCli floor is declared.",
10
+ };
11
+ }
12
+ const renderedRequired = renderValue(required);
13
+ if (typeof required !== "string" ||
14
+ !MINIMUM_VERSION_RANGE.test(required) ||
15
+ !semver.validRange(required)) {
16
+ return {
17
+ status: "warn",
18
+ message: `requiresHqCli has unsupported value ${renderedRequired}; expected a range in the form \">=X.Y.Z\".`,
19
+ };
20
+ }
21
+ if (!semver.valid(currentVersion)) {
22
+ return {
23
+ status: "warn",
24
+ message: `The running hq-cli version ${renderValue(currentVersion)} is not valid SemVer, so the scaffold CLI floor could not be checked.`,
25
+ };
26
+ }
27
+ if (semver.satisfies(currentVersion, required, { includePrerelease: true })) {
28
+ return {
29
+ status: "pass",
30
+ message: `hq-cli ${currentVersion} satisfies the scaffold requirement ${required}.`,
31
+ };
32
+ }
33
+ const minimum = required.slice(2);
34
+ return {
35
+ status: "fail",
36
+ message: `This scaffold requires hq-cli >= ${minimum} (found ${currentVersion}).`,
37
+ remediation: SCAFFOLD_CLI_UPGRADE_COMMAND,
38
+ };
39
+ }
40
+ function renderValue(value) {
41
+ if (typeof value === "string")
42
+ return JSON.stringify(value);
43
+ try {
44
+ return JSON.stringify(value) ?? String(value);
45
+ }
46
+ catch {
47
+ return String(value);
48
+ }
49
+ }
50
+ //# sourceMappingURL=scaffold-cli-floor.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.272.0",
3
+ "version": "5.273.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,14 +0,0 @@
1
- import type { Command } from "commander";
2
- import { type TrackerStore } from "../lib/lanes/tracker-store.js";
3
- import type { LaneCompanyRef } from "../lib/lanes/types.js";
4
- type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
5
- export interface LaneTrackerImportDeps {
6
- hqRoot: () => string;
7
- resolveCompany: (company?: string) => Promise<LaneCompanyRef>;
8
- trackerEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
9
- now: () => Date;
10
- store?: (projectDirectory: string) => TrackerStore;
11
- }
12
- export declare function registerLaneTrackerImportCommand(parent: Command, deps: LaneTrackerImportDeps, wrap: WrapHandler): void;
13
- export {};
14
- //# sourceMappingURL=lanes-import.d.ts.map