@skrr-ai/cli 0.1.44 → 0.1.46
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/base-command.d.ts +13 -1
- package/dist/base-command.js +37 -3
- package/dist/commands/agents/actions/create.js +11 -4
- package/dist/commands/agents/chat.js +3 -4
- package/dist/commands/browser/cloud-agents.d.ts +20 -0
- package/dist/commands/browser/cloud-agents.js +43 -0
- package/dist/commands/code/handover.d.ts +11 -0
- package/dist/commands/code/handover.js +105 -1
- package/dist/commands/commitments/handover.d.ts +29 -0
- package/dist/commands/commitments/handover.js +99 -0
- package/dist/commands/followups/list.js +1 -1
- package/dist/commands/followups/watch.js +40 -5
- package/dist/commands/initiatives/list.d.ts +1 -0
- package/dist/commands/initiatives/list.js +11 -2
- package/dist/commands/labels/list.d.ts +28 -0
- package/dist/commands/labels/list.js +50 -32
- package/dist/commands/tasks/fork.d.ts +32 -0
- package/dist/commands/tasks/fork.js +171 -0
- package/dist/commands/tasks/handover.d.ts +23 -0
- package/dist/commands/tasks/handover.js +205 -0
- package/dist/commands/tasks/promote.d.ts +23 -0
- package/dist/commands/tasks/promote.js +82 -0
- package/dist/commands/tasks/runs.d.ts +16 -0
- package/dist/commands/tasks/runs.js +46 -1
- package/dist/commands/tasks/show.d.ts +23 -0
- package/dist/commands/tasks/show.js +59 -0
- package/dist/lib/agentic-stream.js +41 -2
- package/dist/lib/api-fetch.d.ts +7 -1
- package/dist/lib/api-fetch.js +9 -2
- package/dist/lib/auth-core-init.js +11 -4
- package/dist/lib/code-handover.d.ts +60 -0
- package/dist/lib/code-handover.js +120 -0
- package/dist/lib/commitments.d.ts +22 -0
- package/dist/lib/commitments.js +42 -0
- package/dist/lib/dedicated-wait.js +22 -1
- package/dist/lib/followups.d.ts +18 -0
- package/dist/lib/followups.js +44 -0
- package/dist/lib/initiatives.d.ts +15 -3
- package/dist/lib/label-ref.d.ts +2 -0
- package/dist/lib/node-adapter.js +6 -0
- package/dist/lib/task-extras.d.ts +44 -0
- package/dist/lib/task-extras.js +73 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +29 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +169 -23
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +91 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +7 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +26 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loginLocalhost.js +177 -26
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +119 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +346 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +29 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +169 -24
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +93 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +7 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loginLocalhost.js +177 -26
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +119 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +333 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +2746 -2711
- package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
- package/oclif.manifest.json +34300 -33843
- package/package.json +2 -2
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* profileStateDir.ts — the ONE definition of where a profile's auth state lives
|
|
3
|
+
* on disk, and the migration for a machine that ended up with two.
|
|
4
|
+
*
|
|
5
|
+
* A profile is this product's unit of credential isolation, and its state has
|
|
6
|
+
* exactly one home:
|
|
7
|
+
*
|
|
8
|
+
* <machine root>/profiles/<profile>/cred-dek-wrapped
|
|
9
|
+
* <machine root>/profiles/<profile>/cred-envelope.lock
|
|
10
|
+
* <machine root>/profiles/<profile>/device-private-key
|
|
11
|
+
* …
|
|
12
|
+
*
|
|
13
|
+
* It lives here, next to `configRoot.ts`, for the same reason that file exists:
|
|
14
|
+
* both binaries need the same answer and neither owns it. `configRoot.ts` made
|
|
15
|
+
* "where is the skrr root" one function; this makes "where is a profile's auth
|
|
16
|
+
* state" one function, because the layer above it had the identical defect one
|
|
17
|
+
* level down.
|
|
18
|
+
*
|
|
19
|
+
* WHAT WENT WRONG (OSK-10314). `credentialEnvelopeBridge` and
|
|
20
|
+
* `deviceIdentityBridge` each wrote `path.join(getAuthConfigDir(), 'profiles',
|
|
21
|
+
* profile)` — two independent spellings of one rule — and `configDir` means
|
|
22
|
+
* different things to the two consumers:
|
|
23
|
+
*
|
|
24
|
+
* - the CLI passes the MACHINE ROOT, so it landed on the layout above;
|
|
25
|
+
* - the daemon passes `getConfigDir()`, which is ALREADY `<root>/profiles/<p>`,
|
|
26
|
+
* so it landed on `<root>/profiles/<p>/profiles/<p>/…`.
|
|
27
|
+
*
|
|
28
|
+
* One profile therefore had two wrapped DEKs, two device keypairs and two
|
|
29
|
+
* lockfiles, and the lockfile the shared module exists to share served neither
|
|
30
|
+
* of the two processes it was meant to serialize. Nothing cross-read, so
|
|
31
|
+
* nothing broke — it was a split-brain waiting for the first cross-read.
|
|
32
|
+
*
|
|
33
|
+
* The fix is to derive the directory from `getAuthMachineConfigDir()`, which
|
|
34
|
+
* BOTH consumers already wire explicitly (daemon: `getMachineConfigDir()`; CLI:
|
|
35
|
+
* the skrr root). Note what is deliberately NOT done: the daemon is not asked to
|
|
36
|
+
* pass a different `configDir`. It has one knob and auth-core needs two answers
|
|
37
|
+
* from it — `refresh.ts` reads `getAuthConfigDir()` raw for `auth.lock` and
|
|
38
|
+
* `needs-reauth.json`, so repointing the daemon's `configDir` at the machine
|
|
39
|
+
* root would quietly move its reauth latch from per-profile to machine-wide and
|
|
40
|
+
* let one profile latch every profile. Profile-scoped and root-scoped artifacts
|
|
41
|
+
* are only distinguishable INSIDE auth-core, so that is where the line is drawn.
|
|
42
|
+
*
|
|
43
|
+
* WHY A BARE PATH SWAP IS NOT THE FIX. A daemon with a nested wrapped DEK has
|
|
44
|
+
* sealed real credentials under it. Pointing it at the canonical path hands it a
|
|
45
|
+
* DIFFERENT DEK and every one of those credentials stops decrypting — a silent,
|
|
46
|
+
* fleet-wide forced re-login, to repair a bug that had so far harmed nobody. So
|
|
47
|
+
* the resolver PREFERS THE FILE A PROCESS IS ALREADY USING until a migration has
|
|
48
|
+
* proved the canonical one is equivalent, and the migration COPIES.
|
|
49
|
+
*
|
|
50
|
+
* Resolution order for `<filename>` under `<profile>` (stateless — no ordering
|
|
51
|
+
* requirement between this and any init):
|
|
52
|
+
*
|
|
53
|
+
* 1. legacy dir IS the canonical dir (the CLI) → canonical
|
|
54
|
+
* 2. no legacy copy of the file → canonical
|
|
55
|
+
* 3. a `<filename>.superseded.json` marker beside it → canonical (consolidated)
|
|
56
|
+
* 4. no canonical copy of the file → legacy (not yet consolidated)
|
|
57
|
+
* 5. both copies, no marker → legacy (unresolved: stay put)
|
|
58
|
+
*
|
|
59
|
+
* The migration writes the marker, never deletes, and refuses case 5 when both
|
|
60
|
+
* sides hold a usable and DIFFERENT key — because merging those would require
|
|
61
|
+
* re-sealing content that lives in the OS keychain and is not auth-core's to
|
|
62
|
+
* re-seal. A human resolves that one, and the loud signal is the deliverable.
|
|
63
|
+
*
|
|
64
|
+
* ROLLBACK. The legacy file is left exactly where it was, under its own name.
|
|
65
|
+
* Older code looks there, finds it, and works — no operator step. The marker is
|
|
66
|
+
* a sidecar older code never reads.
|
|
67
|
+
*/
|
|
68
|
+
import { createHash } from 'node:crypto';
|
|
69
|
+
import fs from 'node:fs';
|
|
70
|
+
import path from 'node:path';
|
|
71
|
+
import { getAuthConfigDir, getAuthMachineConfigDir } from './runtime.js';
|
|
72
|
+
/** Directory segment that separates per-profile state from machine-scoped state. */
|
|
73
|
+
export const PROFILES_SEGMENT = 'profiles';
|
|
74
|
+
/**
|
|
75
|
+
* Sidecar written beside a legacy file once it has been consolidated. Its
|
|
76
|
+
* PRESENCE is what flips resolution to the canonical copy; its contents are for
|
|
77
|
+
* a human reading the directory afterwards.
|
|
78
|
+
*/
|
|
79
|
+
export const SUPERSEDED_MARKER_SUFFIX = '.superseded.json';
|
|
80
|
+
/**
|
|
81
|
+
* Suffix for a canonical file moved aside to make room. Only used when the
|
|
82
|
+
* canonical copy existed and was NOT usable — a usable one is never replaced.
|
|
83
|
+
*/
|
|
84
|
+
export const SUPERSEDED_COPY_SUFFIX = '.superseded';
|
|
85
|
+
/** The canonical directory for a profile's auth state. */
|
|
86
|
+
export function profileStateDir(profile) {
|
|
87
|
+
assertProfile(profile);
|
|
88
|
+
return path.join(getAuthMachineConfigDir(), PROFILES_SEGMENT, profile);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Where a consumer whose `configDir` is already profile-scoped used to write.
|
|
92
|
+
*
|
|
93
|
+
* `null` when it resolves to the canonical directory — i.e. the consumer passes
|
|
94
|
+
* the machine root and never had a second location. Callers branch on `null`
|
|
95
|
+
* rather than comparing strings so the "one root" case costs one check.
|
|
96
|
+
*/
|
|
97
|
+
export function legacyProfileStateDir(profile) {
|
|
98
|
+
assertProfile(profile);
|
|
99
|
+
const legacy = path.join(getAuthConfigDir(), PROFILES_SEGMENT, profile);
|
|
100
|
+
const canonical = profileStateDir(profile);
|
|
101
|
+
return path.resolve(legacy) === path.resolve(canonical) ? null : legacy;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the directory holding `primaryFilename` for `profile`.
|
|
105
|
+
*
|
|
106
|
+
* `primaryFilename` is the file whose presence decides where a group of related
|
|
107
|
+
* files lives. Device identity has four files that must be read from ONE
|
|
108
|
+
* directory — a public JWK from one copy and a private key from the other is a
|
|
109
|
+
* keypair mismatch — so the group anchors on the private key rather than each
|
|
110
|
+
* file answering for itself.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveProfileStateDir(profile, primaryFilename) {
|
|
113
|
+
const canonicalDir = profileStateDir(profile);
|
|
114
|
+
const legacyDir = legacyProfileStateDir(profile);
|
|
115
|
+
if (legacyDir === null) {
|
|
116
|
+
return { dir: canonicalDir, canonicalDir, legacyDir: null, usingLegacy: false };
|
|
117
|
+
}
|
|
118
|
+
const legacyFile = path.join(legacyDir, primaryFilename);
|
|
119
|
+
const stayOnLegacy = existsFile(legacyFile) && !existsFile(`${legacyFile}${SUPERSEDED_MARKER_SUFFIX}`);
|
|
120
|
+
return {
|
|
121
|
+
dir: stayOnLegacy ? legacyDir : canonicalDir,
|
|
122
|
+
canonicalDir,
|
|
123
|
+
legacyDir,
|
|
124
|
+
usingLegacy: stayOnLegacy,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** `resolveProfileStateDir` for a single self-anchoring file. */
|
|
128
|
+
export function resolveProfileStateFile(profile, filename) {
|
|
129
|
+
const resolved = resolveProfileStateDir(profile, filename);
|
|
130
|
+
return {
|
|
131
|
+
...resolved,
|
|
132
|
+
path: path.join(resolved.dir, filename),
|
|
133
|
+
canonicalPath: path.join(resolved.canonicalDir, filename),
|
|
134
|
+
legacyPath: resolved.legacyDir === null ? null : path.join(resolved.legacyDir, filename),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Every location `filename` may occupy for `profile`, canonical first. */
|
|
138
|
+
export function profileStateFileLocations(profile, filename) {
|
|
139
|
+
const canonicalDir = profileStateDir(profile);
|
|
140
|
+
const legacyDir = legacyProfileStateDir(profile);
|
|
141
|
+
const out = [path.join(canonicalDir, filename)];
|
|
142
|
+
if (legacyDir !== null)
|
|
143
|
+
out.push(path.join(legacyDir, filename));
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Consolidate a profile's state onto the canonical path. Idempotent, additive,
|
|
148
|
+
* and refuses rather than guesses.
|
|
149
|
+
*
|
|
150
|
+
* The one case it will not decide is two usable, different keys for one profile:
|
|
151
|
+
* each side has sealed content under its own, that content lives in the OS
|
|
152
|
+
* keychain, and nothing here can re-seal it. Picking a winner there would
|
|
153
|
+
* destroy the loser's credentials silently, which is the exact failure this
|
|
154
|
+
* whole module exists to avoid.
|
|
155
|
+
*/
|
|
156
|
+
export async function consolidateProfileState(profile, spec) {
|
|
157
|
+
assertProfile(profile);
|
|
158
|
+
const dryRun = spec.dryRun === true;
|
|
159
|
+
const canonicalDir = profileStateDir(profile);
|
|
160
|
+
const legacyDir = legacyProfileStateDir(profile);
|
|
161
|
+
const base = { profile, canonicalDir, legacyDir, written: [], supersededAside: [], dryRun };
|
|
162
|
+
if (legacyDir === null)
|
|
163
|
+
return { ...base, status: 'not_applicable' };
|
|
164
|
+
const legacyPrimary = path.join(legacyDir, spec.primary);
|
|
165
|
+
if (!existsFile(legacyPrimary))
|
|
166
|
+
return { ...base, status: 'nothing_to_migrate' };
|
|
167
|
+
const markerPath = `${legacyPrimary}${SUPERSEDED_MARKER_SUFFIX}`;
|
|
168
|
+
if (existsFile(markerPath))
|
|
169
|
+
return { ...base, status: 'already_consolidated' };
|
|
170
|
+
const canonicalPrimary = path.join(canonicalDir, spec.primary);
|
|
171
|
+
if (!existsFile(canonicalPrimary)) {
|
|
172
|
+
const moved = adopt(legacyDir, canonicalDir, spec.files, dryRun);
|
|
173
|
+
if (!dryRun)
|
|
174
|
+
writeMarker(markerPath, canonicalPrimary, 'adopted');
|
|
175
|
+
emitOutcome(spec, 'adopted', { profile, from: legacyDir, to: canonicalDir });
|
|
176
|
+
return { ...base, status: 'adopted', ...moved };
|
|
177
|
+
}
|
|
178
|
+
const identify = spec.identify ?? ((dir) => sha256OfFile(path.join(dir, spec.primary)));
|
|
179
|
+
const legacyIdentity = await identify(legacyDir);
|
|
180
|
+
const canonicalIdentity = await identify(canonicalDir);
|
|
181
|
+
const identities = { canonical: canonicalIdentity, legacy: legacyIdentity };
|
|
182
|
+
// Same state in both places, or the legacy copy is unreadable while the
|
|
183
|
+
// canonical one is fine. Either way the canonical copy is authoritative and
|
|
184
|
+
// the legacy one has nothing to contribute — record that and move on.
|
|
185
|
+
const sameState = legacyIdentity !== null && canonicalIdentity !== null && legacyIdentity === canonicalIdentity;
|
|
186
|
+
const legacyHasNothingToOffer = legacyIdentity === null && canonicalIdentity !== null;
|
|
187
|
+
const bothUnreadableButIdentical = legacyIdentity === null &&
|
|
188
|
+
canonicalIdentity === null &&
|
|
189
|
+
sha256OfFile(legacyPrimary) !== null &&
|
|
190
|
+
sha256OfFile(legacyPrimary) === sha256OfFile(canonicalPrimary);
|
|
191
|
+
if (sameState || legacyHasNothingToOffer || bothUnreadableButIdentical) {
|
|
192
|
+
if (!dryRun)
|
|
193
|
+
writeMarker(markerPath, canonicalPrimary, 'already_consolidated');
|
|
194
|
+
emitOutcome(spec, 'already_consolidated', { profile, identities });
|
|
195
|
+
return { ...base, status: 'already_consolidated', identities };
|
|
196
|
+
}
|
|
197
|
+
// The legacy copy is usable and the canonical one is not. Nothing is sealed
|
|
198
|
+
// under an unreadable key that was not already lost, so replacing it costs
|
|
199
|
+
// nothing — but keep it, because "unreadable here, today" is not "worthless".
|
|
200
|
+
if (legacyIdentity !== null && canonicalIdentity === null) {
|
|
201
|
+
const aside = dryRun
|
|
202
|
+
? plannedAside(canonicalDir, spec.files)
|
|
203
|
+
: moveAside(canonicalDir, spec.files);
|
|
204
|
+
const moved = adopt(legacyDir, canonicalDir, spec.files, dryRun);
|
|
205
|
+
if (!dryRun)
|
|
206
|
+
writeMarker(markerPath, canonicalPrimary, 'superseded');
|
|
207
|
+
emitOutcome(spec, 'superseded', { profile, identities, supersededAside: aside });
|
|
208
|
+
return {
|
|
209
|
+
...base,
|
|
210
|
+
status: 'superseded',
|
|
211
|
+
written: moved.written,
|
|
212
|
+
supersededAside: aside,
|
|
213
|
+
identities,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
emitOutcome(spec, 'conflict', { profile, identities, canonicalDir, legacyDir });
|
|
217
|
+
return { ...base, status: 'conflict', identities };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Every path a profile's copy of `filename` can occupy, plus its marker —
|
|
221
|
+
* what a genuine logout must remove. Deliberately separate from the migration:
|
|
222
|
+
* this module never deletes, and logout is the one caller that must.
|
|
223
|
+
*/
|
|
224
|
+
export function profileStateFilePathsToClear(profile, filename) {
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const location of profileStateFileLocations(profile, filename)) {
|
|
227
|
+
out.push(location);
|
|
228
|
+
out.push(`${location}${SUPERSEDED_MARKER_SUFFIX}`);
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// internals
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
function assertProfile(profile) {
|
|
236
|
+
if (typeof profile !== 'string' || profile.length === 0) {
|
|
237
|
+
throw new Error('profileStateDir: profile must be a non-empty string');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function existsFile(target) {
|
|
241
|
+
try {
|
|
242
|
+
return fs.statSync(target).isFile();
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function sha256OfFile(target) {
|
|
249
|
+
try {
|
|
250
|
+
return createHash('sha256').update(fs.readFileSync(target)).digest('hex');
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* tmp + fsync + rename + dir-fsync, 0600. The same shape the two bridges
|
|
258
|
+
* already use, so a crash mid-migration leaves either the old file or the new
|
|
259
|
+
* one and never a half-written key.
|
|
260
|
+
*/
|
|
261
|
+
function atomicWrite0600(target, value) {
|
|
262
|
+
const dir = path.dirname(target);
|
|
263
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
264
|
+
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
265
|
+
const fd = fs.openSync(tmp, 'w', 0o600);
|
|
266
|
+
try {
|
|
267
|
+
fs.writeSync(fd, value);
|
|
268
|
+
fs.fsyncSync(fd);
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
fs.closeSync(fd);
|
|
272
|
+
}
|
|
273
|
+
fs.renameSync(tmp, target);
|
|
274
|
+
try {
|
|
275
|
+
const dirFd = fs.openSync(dir, 'r');
|
|
276
|
+
try {
|
|
277
|
+
fs.fsyncSync(dirFd);
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
fs.closeSync(dirFd);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
/* best-effort — Windows / some BSDs reject dir fsync */
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function adopt(fromDir, toDir, files, dryRun) {
|
|
288
|
+
const written = [];
|
|
289
|
+
for (const name of files) {
|
|
290
|
+
const source = path.join(fromDir, name);
|
|
291
|
+
if (!existsFile(source))
|
|
292
|
+
continue;
|
|
293
|
+
const target = path.join(toDir, name);
|
|
294
|
+
if (!dryRun)
|
|
295
|
+
atomicWrite0600(target, fs.readFileSync(source));
|
|
296
|
+
written.push(target);
|
|
297
|
+
}
|
|
298
|
+
return { written, supersededAside: [] };
|
|
299
|
+
}
|
|
300
|
+
function moveAside(dir, files) {
|
|
301
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
302
|
+
const aside = [];
|
|
303
|
+
for (const name of files) {
|
|
304
|
+
const target = path.join(dir, name);
|
|
305
|
+
if (!existsFile(target))
|
|
306
|
+
continue;
|
|
307
|
+
fs.renameSync(target, `${target}${SUPERSEDED_COPY_SUFFIX}.${stamp}`);
|
|
308
|
+
aside.push(target);
|
|
309
|
+
}
|
|
310
|
+
return aside;
|
|
311
|
+
}
|
|
312
|
+
function plannedAside(dir, files) {
|
|
313
|
+
return files.map((name) => path.join(dir, name)).filter((target) => existsFile(target));
|
|
314
|
+
}
|
|
315
|
+
function writeMarker(markerPath, supersededBy, reason) {
|
|
316
|
+
const payload = {
|
|
317
|
+
supersededBy,
|
|
318
|
+
reason,
|
|
319
|
+
sha256: sha256OfFile(supersededBy),
|
|
320
|
+
at: new Date().toISOString(),
|
|
321
|
+
note: 'Kept deliberately. This file is the pre-consolidation copy (OSK-10314); ' +
|
|
322
|
+
'older binaries read it and it is the only rollback that needs no operator step. ' +
|
|
323
|
+
'Remove it only through logout.',
|
|
324
|
+
};
|
|
325
|
+
atomicWrite0600(markerPath, Buffer.from(`${JSON.stringify(payload, null, 2)}\n`, 'utf-8'));
|
|
326
|
+
}
|
|
327
|
+
function emitOutcome(spec, status, meta) {
|
|
328
|
+
spec.onEvent?.(`path_consolidation.${status}`, status !== 'conflict', {
|
|
329
|
+
...meta,
|
|
330
|
+
primary: spec.primary,
|
|
331
|
+
...(spec.dryRun === true ? { dryRun: true } : {}),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skrr-ai/auth-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"main": "dist/cjs/index.js",
|
|
5
5
|
"types": "dist/esm/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -33,6 +33,16 @@
|
|
|
33
33
|
"types": "./dist/cjs/firstPartyHarness.d.ts",
|
|
34
34
|
"default": "./dist/cjs/firstPartyHarness.js"
|
|
35
35
|
}
|
|
36
|
+
},
|
|
37
|
+
"./harness-skill-layout": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/esm/harnessSkillLayout.d.ts",
|
|
40
|
+
"default": "./dist/esm/harnessSkillLayout.js"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/cjs/harnessSkillLayout.d.ts",
|
|
44
|
+
"default": "./dist/cjs/harnessSkillLayout.js"
|
|
45
|
+
}
|
|
36
46
|
}
|
|
37
47
|
},
|
|
38
48
|
"dependencies": {
|