@indigoai-us/hq-cli 5.196.0 → 5.198.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/CHANGELOG.md +37 -0
- package/dist/command-catalog.generated.d.ts +390 -0
- package/dist/command-catalog.generated.js +506 -0
- package/dist/commands/index-cmd.d.ts +7 -1
- package/dist/commands/index-cmd.js +46 -21
- package/dist/commands/lanes-tracker.d.ts +20 -0
- package/dist/commands/lanes-tracker.js +420 -0
- package/dist/commands/lanes.d.ts +6 -0
- package/dist/commands/lanes.js +18 -0
- package/dist/lib/bot/inflight.d.ts +12 -0
- package/dist/lib/bot/run.js +21 -2
- package/dist/lib/bot/runtime/messages-stream.d.ts +21 -1
- package/dist/lib/bot/runtime/messages-stream.js +54 -2
- package/dist/lib/core-utils/qmd-reindex-after-sync.d.ts +3 -1
- package/dist/lib/core-utils/qmd-reindex-after-sync.js +69 -40
- package/dist/lib/flag-registry-worker.js +12 -1
- package/dist/lib/flag-registry.js +72 -4
- package/dist/lib/lanes/story-ledger.js +52 -11
- package/dist/lib/lanes/tracker-store.d.ts +27 -0
- package/dist/lib/lanes/tracker-store.js +277 -0
- package/dist/lib/search-index/embed-lock.d.ts +5 -4
- package/dist/lib/search-index/embed-lock.js +5 -4
- package/dist/lib/search-index/reindex-single-flight-flag.d.ts +5 -0
- package/dist/lib/search-index/reindex-single-flight-flag.js +8 -0
- package/dist/node-network-compat-flag.d.ts +19 -0
- package/dist/node-network-compat-flag.js +87 -0
- package/dist/node-network-compat.d.ts +13 -16
- package/dist/node-network-compat.js +74 -19
- package/package.json +1 -1
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Contract preserved verbatim: a missing qmd or a non-HQ path is a silent
|
|
9
9
|
* success (a sync must never fail because indexing could not run), the lexical
|
|
10
|
-
* update
|
|
10
|
+
* update normally runs, and embeddings rebuild only on `--embed`. An enabled
|
|
11
|
+
* single-flight canary may skip a pass while another QMD writer owns the lock.
|
|
11
12
|
*/
|
|
12
13
|
import * as fs from "node:fs";
|
|
13
14
|
import * as path from "node:path";
|
|
@@ -16,6 +17,7 @@ import { applyIndexSizeLimit } from "../search-index/max-doc-bytes.js";
|
|
|
16
17
|
import { defaultEmbedLockDependencies, runWithEmbedLock, } from "../search-index/embed-lock.js";
|
|
17
18
|
import { LoadGateConfigurationError, checkEmbedLoad, defaultLoadGateDependencies, recordSuccessfulEmbed, } from "../search-index/load-gate.js";
|
|
18
19
|
import { resolveProcessFlagGate } from "../flag-registry.js";
|
|
20
|
+
import { qmdReindexSingleFlightEnabled } from "../search-index/reindex-single-flight-flag.js";
|
|
19
21
|
import { isQmdNativeBindingError, QMD_NATIVE_BINDING_REMEDY, } from "../../utils/qmd-native-binding-error.js";
|
|
20
22
|
import { ioFor, line } from "./common.js";
|
|
21
23
|
export const QMD_REINDEX_STARTUP_WARNING_FLAG = "qmd.reindex-startup-warning";
|
|
@@ -58,52 +60,59 @@ export function qmdReindexAfterSync(args = [], options = {}) {
|
|
|
58
60
|
startupFailureWarningShown = true;
|
|
59
61
|
line(stderr, `hq: search index was not updated after sync; ${QMD_NATIVE_BINDING_REMEDY}`);
|
|
60
62
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
63
|
+
const updateIndex = () => {
|
|
64
|
+
// Every qmd interaction is best-effort: the shell suffixed each with `|| true`.
|
|
65
|
+
try {
|
|
66
|
+
reconcile(hqRoot, { bin });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* registration is advisory; an update still helps */
|
|
70
|
+
}
|
|
71
|
+
// The cap has to land before the update: qmd decides what to read
|
|
72
|
+
// at glob time, so an ignore entry written afterwards saves nothing on this
|
|
73
|
+
// pass. This is the post-sync entry point, which reaches `qmd update` without
|
|
74
|
+
// going through `hq index sync` at all.
|
|
75
|
+
try {
|
|
76
|
+
sizeLimit(hqRoot);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* an uncapped index is worse than none, but still better than a failed sync */
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
run(["update"], { bin, cwd: hqRoot });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
warnOnNativeStartupFailure(error);
|
|
86
|
+
/* never fail a sync on index freshness */
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const runEmbed = (home, lockDependencies) => {
|
|
90
|
+
const loadGateDependencies = options.loadGateDependencies ?? defaultLoadGateDependencies(lockDependencies.env);
|
|
91
|
+
const decision = checkEmbedLoad(home, loadGateDependencies);
|
|
92
|
+
if (decision.action === "skip") {
|
|
93
|
+
line(stderr, decision.notice.trimEnd());
|
|
94
|
+
return "skipped";
|
|
95
|
+
}
|
|
96
|
+
if (decision.forced)
|
|
97
|
+
line(stderr, decision.notice.trimEnd());
|
|
98
|
+
run(["embed"], { bin, cwd: hqRoot });
|
|
99
|
+
recordSuccessfulEmbed(home, loadGateDependencies);
|
|
100
|
+
return undefined;
|
|
101
|
+
};
|
|
102
|
+
if ((options.singleFlightEnabled ?? qmdReindexSingleFlightEnabled)()) {
|
|
86
103
|
const lockDependencies = options.embedLockDependencies ?? defaultEmbedLockDependencies();
|
|
87
104
|
const home = resolveQmdHome(lockDependencies.env);
|
|
88
|
-
const loadGateDependencies = options.loadGateDependencies ?? defaultLoadGateDependencies(lockDependencies.env);
|
|
89
105
|
try {
|
|
90
106
|
const outcome = runWithEmbedLock(home, lockDependencies, () => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
line(stderr, decision.notice.trimEnd());
|
|
94
|
-
return "skipped";
|
|
95
|
-
}
|
|
96
|
-
if (decision.forced)
|
|
97
|
-
line(stderr, decision.notice.trimEnd());
|
|
98
|
-
run(["embed"], { bin, cwd: hqRoot });
|
|
99
|
-
recordSuccessfulEmbed(home, loadGateDependencies);
|
|
100
|
-
return undefined;
|
|
107
|
+
updateIndex();
|
|
108
|
+
return embed ? runEmbed(home, lockDependencies) : undefined;
|
|
101
109
|
});
|
|
102
110
|
if (outcome === "busy") {
|
|
103
|
-
line(stderr, "hq: qmd
|
|
111
|
+
line(stderr, "hq: qmd reindex is already running; skipping this post-sync pass.");
|
|
112
|
+
}
|
|
113
|
+
if (outcome === "unavailable") {
|
|
114
|
+
line(stderr, "hq: cannot access the qmd reindex lock; skipping this post-sync pass.");
|
|
104
115
|
}
|
|
105
|
-
if (outcome === "unavailable")
|
|
106
|
-
line(stderr, "hq: cannot access the qmd embed lock; skipping this pass.");
|
|
107
116
|
}
|
|
108
117
|
catch (error) {
|
|
109
118
|
if (error instanceof LoadGateConfigurationError)
|
|
@@ -111,6 +120,26 @@ export function qmdReindexAfterSync(args = [], options = {}) {
|
|
|
111
120
|
warnOnNativeStartupFailure(error);
|
|
112
121
|
/* embeddings are deferred-cost and optional */
|
|
113
122
|
}
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
updateIndex();
|
|
126
|
+
if (!embed)
|
|
127
|
+
return 0;
|
|
128
|
+
const lockDependencies = options.embedLockDependencies ?? defaultEmbedLockDependencies();
|
|
129
|
+
const home = resolveQmdHome(lockDependencies.env);
|
|
130
|
+
try {
|
|
131
|
+
const outcome = runWithEmbedLock(home, lockDependencies, () => runEmbed(home, lockDependencies));
|
|
132
|
+
if (outcome === "busy") {
|
|
133
|
+
line(stderr, "hq: qmd embed is already running; skipping this pass.");
|
|
134
|
+
}
|
|
135
|
+
if (outcome === "unavailable")
|
|
136
|
+
line(stderr, "hq: cannot access the qmd embed lock; skipping this pass.");
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof LoadGateConfigurationError)
|
|
140
|
+
line(stderr, `hq: ${error.message}`);
|
|
141
|
+
warnOnNativeStartupFailure(error);
|
|
142
|
+
/* embeddings are deferred-cost and optional */
|
|
114
143
|
}
|
|
115
144
|
return 0;
|
|
116
145
|
}
|
|
@@ -2,13 +2,24 @@
|
|
|
2
2
|
import { parentPort, workerData } from "node:worker_threads";
|
|
3
3
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
4
4
|
import { createFlagClient } from "@indigoai-us/hq-flags-client";
|
|
5
|
-
const
|
|
5
|
+
const data = workerData;
|
|
6
|
+
const endpoint = data.endpoint;
|
|
7
|
+
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
8
|
+
const COMPANY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
6
9
|
async function refresh() {
|
|
7
10
|
if (!parentPort || typeof endpoint !== "string" || endpoint.trim() === "")
|
|
8
11
|
return;
|
|
9
12
|
try {
|
|
13
|
+
const companyUid = typeof data.companyUid === "string" && COMPANY_UID_RE.test(data.companyUid)
|
|
14
|
+
? data.companyUid
|
|
15
|
+
: undefined;
|
|
16
|
+
const companyIdentifiers = Array.isArray(data.companyIdentifiers)
|
|
17
|
+
? data.companyIdentifiers.filter((value) => typeof value === "string" && COMPANY_SLUG_RE.test(value))
|
|
18
|
+
: [];
|
|
10
19
|
const client = createFlagClient({
|
|
11
20
|
endpoint,
|
|
21
|
+
...(companyUid ? { companyUid } : {}),
|
|
22
|
+
...(companyIdentifiers.length ? { companyIdentifiers } : {}),
|
|
12
23
|
getToken: () => loadCachedTokens()?.idToken ?? "",
|
|
13
24
|
refreshIntervalMs: 0,
|
|
14
25
|
onError: () => { },
|
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Worker } from "node:worker_threads";
|
|
9
9
|
import { isFlagSnapshot, readLocalOverride, resolveFlag, } from "@indigoai-us/hq-flags-client";
|
|
10
|
+
import { readMachineCompanyBinding } from "./work-context/company.js";
|
|
11
|
+
import { readSessionState } from "./work-context/state.js";
|
|
12
|
+
import { workContextRoot } from "./work-context/paths.js";
|
|
13
|
+
import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
|
|
14
|
+
import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
10
15
|
/** Dedicated endpoint configuration; this is intentionally not HQ_PRO_API_URL. */
|
|
11
16
|
export const FLAG_REGISTRY_ENDPOINT_ENV = "HQ_FLAGS_API_URL";
|
|
17
|
+
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
18
|
+
const COMPANY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
12
19
|
let heldSnapshot = null;
|
|
13
20
|
let processReader = null;
|
|
14
21
|
function processLocalOverride(flagKey) {
|
|
@@ -17,14 +24,66 @@ function processLocalOverride(flagKey) {
|
|
|
17
24
|
function localOverrideOrFallback(flagKey, legacyFallback) {
|
|
18
25
|
return processLocalOverride(flagKey) ?? legacyFallback();
|
|
19
26
|
}
|
|
20
|
-
function
|
|
27
|
+
function normalizedCompanyUid(value) {
|
|
28
|
+
const candidate = value?.trim() ?? "";
|
|
29
|
+
return COMPANY_UID_RE.test(candidate) ? candidate : undefined;
|
|
30
|
+
}
|
|
31
|
+
function normalizedCompanySlug(value) {
|
|
32
|
+
const candidate = value?.trim() ?? "";
|
|
33
|
+
return COMPANY_SLUG_RE.test(candidate) ? candidate : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve only an explicit machine/session company binding. Device defaults
|
|
37
|
+
* and cwd/repository heuristics do not scope process-wide feature flags.
|
|
38
|
+
*/
|
|
39
|
+
function processCompanyContext(env = process.env) {
|
|
40
|
+
const explicitUid = normalizedCompanyUid(env.HQ_COMPANY_UID);
|
|
41
|
+
const explicitSlug = normalizedCompanySlug(env.HQ_COMPANY_SLUG);
|
|
42
|
+
if (explicitUid || explicitSlug) {
|
|
43
|
+
return { companyUid: explicitUid, companySlug: explicitSlug };
|
|
44
|
+
}
|
|
45
|
+
const machine = readMachineCompanyBinding(env);
|
|
46
|
+
const spawnValue = env.HQ_SPAWN_COMPANY?.trim();
|
|
47
|
+
const spawnSlug = normalizedCompanySlug(spawnValue);
|
|
48
|
+
const machineContext = spawnValue && COMPANY_UID_RE.test(spawnValue)
|
|
49
|
+
? { companyUid: spawnValue }
|
|
50
|
+
: spawnSlug
|
|
51
|
+
? { companySlug: spawnSlug }
|
|
52
|
+
: {
|
|
53
|
+
companyUid: normalizedCompanyUid(machine?.uid),
|
|
54
|
+
companySlug: normalizedCompanySlug(machine?.slug),
|
|
55
|
+
};
|
|
56
|
+
if (machineContext.companyUid || machineContext.companySlug)
|
|
57
|
+
return machineContext;
|
|
58
|
+
const sessionId = env.HQ_SESSION_ID?.trim();
|
|
59
|
+
if (!sessionId)
|
|
60
|
+
return {};
|
|
61
|
+
const session = readSessionState(sessionId, workContextRoot(undefined, env));
|
|
62
|
+
if (session?.contextStatus !== "bound" || session.companyConfidence === "device_default") {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
companyUid: normalizedCompanyUid(session.companyUid),
|
|
67
|
+
companySlug: normalizedCompanySlug(session.companySlug),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function companyUidForContext(context, env = process.env) {
|
|
71
|
+
if (context.companyUid)
|
|
72
|
+
return context.companyUid;
|
|
73
|
+
if (!context.companySlug)
|
|
74
|
+
return undefined;
|
|
75
|
+
const hqRoot = resolveDefaultHqRoot({ env });
|
|
76
|
+
const manifestUid = readCompaniesManifestMap(hqRoot)?.[context.companySlug]?.cloud_uid;
|
|
77
|
+
return typeof manifestUid === "string" ? normalizedCompanyUid(manifestUid) : undefined;
|
|
78
|
+
}
|
|
79
|
+
function processSnapshotReader(companyIdentifiers) {
|
|
21
80
|
return {
|
|
22
81
|
isEnabled(flagKey, lookup = {}) {
|
|
23
82
|
return resolveFlag({
|
|
24
83
|
flagKey,
|
|
25
84
|
env: process.env,
|
|
26
85
|
snapshot: heldSnapshot,
|
|
27
|
-
companyIdentifiers
|
|
86
|
+
companyIdentifiers,
|
|
28
87
|
lookup,
|
|
29
88
|
}).value;
|
|
30
89
|
},
|
|
@@ -45,9 +104,18 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
45
104
|
const endpoint = (dependencies.endpoint ?? process.env[FLAG_REGISTRY_ENDPOINT_ENV] ?? "").trim();
|
|
46
105
|
if (!endpoint)
|
|
47
106
|
return null;
|
|
48
|
-
|
|
107
|
+
const company = processCompanyContext();
|
|
108
|
+
const companyUid = companyUidForContext(company);
|
|
109
|
+
const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
|
|
110
|
+
processReader = processSnapshotReader(companyIdentifiers);
|
|
49
111
|
try {
|
|
50
|
-
const worker = (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(new URL("./flag-registry-worker.js", import.meta.url), {
|
|
112
|
+
const worker = (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(new URL("./flag-registry-worker.js", import.meta.url), {
|
|
113
|
+
workerData: {
|
|
114
|
+
endpoint,
|
|
115
|
+
...(companyUid ? { companyUid } : {}),
|
|
116
|
+
...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
|
|
117
|
+
},
|
|
118
|
+
});
|
|
51
119
|
worker.on("message", (value) => {
|
|
52
120
|
if (isFlagSnapshot(value))
|
|
53
121
|
heldSnapshot = value;
|
|
@@ -122,6 +122,11 @@ function lockPathFor(filePath) {
|
|
|
122
122
|
function lockOwnerPath(lockPath) {
|
|
123
123
|
return path.join(lockPath, "owner.json");
|
|
124
124
|
}
|
|
125
|
+
function isInvalidLockOwnerReceipt(err, lockPath) {
|
|
126
|
+
return err instanceof LanesError &&
|
|
127
|
+
err.error === "story_ledger_lock_invalid" &&
|
|
128
|
+
err.details.path === lockOwnerPath(lockPath);
|
|
129
|
+
}
|
|
125
130
|
function readLockOwner(lockPath) {
|
|
126
131
|
const ownerPath = lockOwnerPath(lockPath);
|
|
127
132
|
let contents;
|
|
@@ -157,15 +162,17 @@ function lockSleep(deps, ms) {
|
|
|
157
162
|
}
|
|
158
163
|
function restoreMovedLockWithoutReplacement(lockPath, stalePath) {
|
|
159
164
|
const entries = fs.readdirSync(stalePath);
|
|
160
|
-
|
|
165
|
+
const isOwnerTemp = (entry) => /^\.owner-[a-f0-9]{24}\.tmp$/.test(entry);
|
|
166
|
+
if (entries.some((entry) => entry !== "owner.json" && !isOwnerTemp(entry))) {
|
|
161
167
|
throw new Error("Unexpected entry in moved story ledger lock");
|
|
162
168
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
:
|
|
169
|
+
const files = entries.map((entry) => ({
|
|
170
|
+
name: entry,
|
|
171
|
+
contents: fs.readFileSync(path.join(stalePath, entry), "utf8"),
|
|
172
|
+
}));
|
|
166
173
|
fs.mkdirSync(lockPath);
|
|
167
|
-
|
|
168
|
-
fs.writeFileSync(
|
|
174
|
+
for (const file of files) {
|
|
175
|
+
fs.writeFileSync(path.join(lockPath, file.name), file.contents, {
|
|
169
176
|
encoding: "utf8",
|
|
170
177
|
flag: "wx",
|
|
171
178
|
mode: 0o600,
|
|
@@ -186,10 +193,18 @@ function clearStaleLock(lockPath, deps) {
|
|
|
186
193
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
187
194
|
throw new LanesError("story_ledger_lock_invalid", "Story ledger lock path is not a regular directory: " + lockPath, { path: lockPath });
|
|
188
195
|
}
|
|
189
|
-
|
|
190
|
-
|
|
196
|
+
let ownerRead;
|
|
197
|
+
try {
|
|
198
|
+
ownerRead = readLockOwner(lockPath);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
if (!isInvalidLockOwnerReceipt(err, lockPath) || lockNow(deps) - stat.mtimeMs >= LOCK_STALE_MS) {
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
191
206
|
let lockAt = stat.mtimeMs;
|
|
192
|
-
if (
|
|
207
|
+
if (hasLockOwnerToken(ownerRead) && typeof ownerRead.owner.at === "string") {
|
|
193
208
|
const parsedAt = Date.parse(ownerRead.owner.at);
|
|
194
209
|
if (Number.isFinite(parsedAt))
|
|
195
210
|
lockAt = parsedAt;
|
|
@@ -207,7 +222,26 @@ function clearStaleLock(lockPath, deps) {
|
|
|
207
222
|
throw err;
|
|
208
223
|
}
|
|
209
224
|
const moved = fs.lstatSync(stalePath);
|
|
210
|
-
|
|
225
|
+
let movedOwnerRead;
|
|
226
|
+
try {
|
|
227
|
+
movedOwnerRead = readLockOwner(stalePath);
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
if (!isInvalidLockOwnerReceipt(err, stalePath))
|
|
231
|
+
throw err;
|
|
232
|
+
const stillFresh = lockNow(deps) - moved.mtimeMs < LOCK_STALE_MS;
|
|
233
|
+
try {
|
|
234
|
+
// Put the moved directory back before retrying or reporting the invalid receipt.
|
|
235
|
+
restoreMovedLockWithoutReplacement(lockPath, stalePath);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
throw new LanesError("story_ledger_lock_race", "A replacement story ledger lock was preserved at " + stalePath +
|
|
239
|
+
" because it could not safely be restored to " + lockPath, { path: lockPath, preserved_path: stalePath });
|
|
240
|
+
}
|
|
241
|
+
if (stillFresh)
|
|
242
|
+
return false;
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
211
245
|
let movedLockAt = moved.mtimeMs;
|
|
212
246
|
if (hasLockOwnerToken(movedOwnerRead) && typeof movedOwnerRead.owner.at === "string") {
|
|
213
247
|
const parsedAt = Date.parse(movedOwnerRead.owner.at);
|
|
@@ -239,7 +273,14 @@ function acquireLedgerLock(filePath, deps) {
|
|
|
239
273
|
while (lockNow(deps) - started < timeoutMs) {
|
|
240
274
|
try {
|
|
241
275
|
fs.mkdirSync(lockPath);
|
|
242
|
-
|
|
276
|
+
const ownerTempPath = path.join(lockPath, ".owner-" + token + ".tmp");
|
|
277
|
+
try {
|
|
278
|
+
fs.writeFileSync(ownerTempPath, JSON.stringify({ pid: process.pid, token, at: new Date(lockNow(deps)).toISOString() }) + "\n", { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
279
|
+
fs.renameSync(ownerTempPath, lockOwnerPath(lockPath));
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
fs.rmSync(ownerTempPath, { force: true });
|
|
283
|
+
}
|
|
243
284
|
return { lockPath, token };
|
|
244
285
|
}
|
|
245
286
|
catch (err) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type TrackerKind = "blocker" | "kpi" | "okr" | "directive";
|
|
2
|
+
export interface TrackerRecord {
|
|
3
|
+
schema_version: "hq-lanes-tracker-record.v1";
|
|
4
|
+
kind: TrackerKind;
|
|
5
|
+
id: string;
|
|
6
|
+
company: string;
|
|
7
|
+
project_id: string;
|
|
8
|
+
version: number;
|
|
9
|
+
created_at: string;
|
|
10
|
+
updated_at: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface TrackerRecordInput {
|
|
14
|
+
kind: TrackerKind;
|
|
15
|
+
id?: string;
|
|
16
|
+
company: string;
|
|
17
|
+
project_id: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface TrackerStore {
|
|
21
|
+
read(kind: TrackerKind, id: string): TrackerRecord | null;
|
|
22
|
+
list(kind: TrackerKind): TrackerRecord[];
|
|
23
|
+
create(record: TrackerRecordInput): TrackerRecord;
|
|
24
|
+
update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord): TrackerRecord;
|
|
25
|
+
}
|
|
26
|
+
export declare function createLocalTrackerStore(projectDirectory: string, now?: () => Date): TrackerStore;
|
|
27
|
+
//# sourceMappingURL=tracker-store.d.ts.map
|