@ngockhoale/ukit 2.7.13 → 2.8.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 +9 -0
- package/manifests/documentation.yaml +11 -0
- package/manifests/platform.full.yaml +182 -0
- package/manifests/platform.user.yaml +53 -0
- package/package.json +3 -1
- package/src/cli/commands/diff.js +4 -2
- package/src/cli/commands/doctor.js +22 -1
- package/src/cli/commands/install.js +10 -0
- package/src/cli/commands/memory.js +142 -3
- package/src/cli/commands/playbook.js +53 -0
- package/src/cli/index.js +7 -0
- package/src/core/memory/recordStore.js +81 -0
- package/src/core/memory/storeV2.js +16 -52
- package/src/core/memory/userMemory.js +111 -0
- package/src/core/paths.js +1 -0
- package/src/core/runInstallPipeline.js +96 -3
- package/src/core/runtimeConfig.js +170 -5
- package/src/core/userPaths.js +21 -0
- package/src/core/userPlaybooks.js +185 -0
- package/src/index/taskRouting.js +422 -21
- package/src/index/verificationPlan.js +17 -0
- package/src/manifest/validateManifest.js +19 -0
- package/templates/.claude/config/providers.md +1 -3
- package/templates/.claude/skills/principle-attack-the-premise/SKILL.md +16 -0
- package/templates/.claude/skills/principle-boundary-discipline/SKILL.md +16 -0
- package/templates/.claude/skills/principle-encode-lessons-in-structure/SKILL.md +16 -0
- package/templates/.claude/skills/principle-fix-root-causes/SKILL.md +18 -0
- package/templates/.claude/skills/principle-foundational-thinking/SKILL.md +17 -0
- package/templates/.claude/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/templates/.claude/skills/principle-laziness-protocol/SKILL.md +17 -0
- package/templates/.claude/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +16 -0
- package/templates/.claude/skills/principle-minimize-reader-load/SKILL.md +17 -0
- package/templates/.claude/skills/principle-model-the-domain/SKILL.md +16 -0
- package/templates/.claude/skills/principle-never-block-on-the-human/SKILL.md +16 -0
- package/templates/.claude/skills/principle-prove-it-works/SKILL.md +18 -0
- package/templates/.claude/skills/principle-sequence-verifiable-units/SKILL.md +16 -0
- package/templates/.claude/skills/principle-subtract-before-you-add/SKILL.md +16 -0
- package/templates/.claude/skills/principle-test-behavior-not-implementation/SKILL.md +18 -0
- package/templates/.claude/ukit/index/route-task.mjs +652 -28
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +238 -9
- package/templates/ukit/README.md +31 -0
- package/templates/ukit/storage/config.json +10 -0
- package/templates/user/README.md +21 -0
- package/templates/user/playbooks/bug-fix.md +18 -0
- package/templates/user/playbooks/issue-implementation.md +14 -0
- package/templates/user/storage/config.json +16 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Record-document store core — SPEC §3 / FR-007.
|
|
2
|
+
// Path-agnostic read/write of the v2 records document
|
|
3
|
+
// ({ schemaVersion: 2, records: [...] }) so both the project store
|
|
4
|
+
// (storeV2.js) and the user store (userMemory.js) share one implementation.
|
|
5
|
+
// Writes go through fileOps.writeJson (tmp+rename).
|
|
6
|
+
|
|
7
|
+
import { readJsonIfExists, writeJson } from '../fileOps.js';
|
|
8
|
+
import { normalizeRecord } from './records.js';
|
|
9
|
+
|
|
10
|
+
const SCHEMA_VERSION = 2;
|
|
11
|
+
|
|
12
|
+
export const PATCHABLE_FIELDS = ['type', 'scope', 'text', 'provenance', 'confidence',
|
|
13
|
+
'valid_until', 'status', 'project_id', 'meta', 'source_fingerprint'];
|
|
14
|
+
|
|
15
|
+
export function isRawRecord(input) {
|
|
16
|
+
return input && typeof input === 'object' && typeof input.id === 'string'
|
|
17
|
+
&& typeof input.status === 'string';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* readRecordStore(recordsPath) → { records, invalidSkipped }
|
|
22
|
+
* Tolerant: missing file → {records:[]}; unparseable → {records:[], invalidSkipped:1};
|
|
23
|
+
* per-record normalizeRecord skip.
|
|
24
|
+
*/
|
|
25
|
+
export async function readRecordStore(recordsPath) {
|
|
26
|
+
let doc;
|
|
27
|
+
try {
|
|
28
|
+
doc = await readJsonIfExists(recordsPath);
|
|
29
|
+
} catch {
|
|
30
|
+
return { records: [], invalidSkipped: 1 };
|
|
31
|
+
}
|
|
32
|
+
if (!doc) return { records: [], invalidSkipped: 0 };
|
|
33
|
+
|
|
34
|
+
const rawRecords = Array.isArray(doc.records) ? doc.records : [];
|
|
35
|
+
const records = [];
|
|
36
|
+
let invalidSkipped = Array.isArray(doc.records) ? 0 : 1;
|
|
37
|
+
for (const raw of rawRecords) {
|
|
38
|
+
const normalized = normalizeRecord(raw);
|
|
39
|
+
if (normalized) records.push(normalized);
|
|
40
|
+
else invalidSkipped += 1;
|
|
41
|
+
}
|
|
42
|
+
return { records, invalidSkipped };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* writeRecordStore(recordsPath, records) → void
|
|
47
|
+
* Atomic write; drops unrecoverable entries.
|
|
48
|
+
*/
|
|
49
|
+
export async function writeRecordStore(recordsPath, records) {
|
|
50
|
+
const normalized = (Array.isArray(records) ? records : [])
|
|
51
|
+
.map((raw) => normalizeRecord(raw))
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
await writeJson(recordsPath, {
|
|
54
|
+
schemaVersion: SCHEMA_VERSION,
|
|
55
|
+
records: normalized,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* applyRecordPatch(records, id, patch) → { record, records } | null
|
|
61
|
+
* Patches PATCHABLE_FIELDS on the record with `id`, re-normalizes, and returns
|
|
62
|
+
* the updated list (caller persists). Throws when the patch is unrecoverable.
|
|
63
|
+
*/
|
|
64
|
+
export function applyRecordPatch(records, id, patch = {}) {
|
|
65
|
+
const index = records.findIndex((r) => r.id === id);
|
|
66
|
+
if (index === -1) return null;
|
|
67
|
+
|
|
68
|
+
const next = { ...records[index] };
|
|
69
|
+
for (const field of PATCHABLE_FIELDS) {
|
|
70
|
+
if (Object.prototype.hasOwnProperty.call(patch, field)) {
|
|
71
|
+
next[field] = patch[field];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const normalized = normalizeRecord(next);
|
|
75
|
+
if (!normalized) {
|
|
76
|
+
throw new Error(`applyRecordPatch: patch produces an invalid record (id ${id})`);
|
|
77
|
+
}
|
|
78
|
+
const updated = [...records];
|
|
79
|
+
updated[index] = normalized;
|
|
80
|
+
return { record: normalized, records: updated };
|
|
81
|
+
}
|
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
// Read ops call ensureMigrated() first so every consumer triggers the lazy
|
|
5
5
|
// v1→v2 migration (SPEC §4); migrate.js is lazy-imported to break the
|
|
6
6
|
// storeV2↔migrate import cycle (runMigration writes via saveRecords).
|
|
7
|
+
// The record-document read/write core lives in recordStore.js (FR-007) so the
|
|
8
|
+
// user-level store (userMemory.js) shares it without the migration flow.
|
|
7
9
|
|
|
8
10
|
import fs from 'node:fs/promises';
|
|
9
11
|
import { buildRuntimePaths } from '../runtimePaths.js';
|
|
10
|
-
import { readJsonIfExists, writeJson } from '../fileOps.js';
|
|
11
12
|
import { loadRuntimeConfig } from '../runtimeConfig.js';
|
|
12
13
|
import { createRecord, normalizeRecord } from './records.js';
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
import {
|
|
15
|
+
readRecordStore,
|
|
16
|
+
writeRecordStore,
|
|
17
|
+
applyRecordPatch,
|
|
18
|
+
isRawRecord,
|
|
19
|
+
} from './recordStore.js';
|
|
15
20
|
|
|
16
21
|
// Per-process memoization for ensureMigrated — one-shot check per projectRoot.
|
|
17
22
|
const migratedRoots = new Set();
|
|
@@ -43,11 +48,6 @@ async function ensureMigrated(projectRoot) {
|
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
function isRawRecord(input) {
|
|
47
|
-
return input && typeof input === 'object' && typeof input.id === 'string'
|
|
48
|
-
&& typeof input.status === 'string';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
51
|
/**
|
|
52
52
|
* loadRecords(projectRoot) → record[] — tolerant: invalid entries skipped.
|
|
53
53
|
*/
|
|
@@ -59,23 +59,7 @@ export async function loadRecords(projectRoot) {
|
|
|
59
59
|
|
|
60
60
|
async function readStore(projectRoot) {
|
|
61
61
|
const paths = buildRuntimePaths(projectRoot);
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
doc = await readJsonIfExists(paths.memoryV2RecordsPath);
|
|
65
|
-
} catch {
|
|
66
|
-
return { records: [], invalidSkipped: 1 };
|
|
67
|
-
}
|
|
68
|
-
if (!doc) return { records: [], invalidSkipped: 0 };
|
|
69
|
-
|
|
70
|
-
const rawRecords = Array.isArray(doc.records) ? doc.records : [];
|
|
71
|
-
const records = [];
|
|
72
|
-
let invalidSkipped = Array.isArray(doc.records) ? 0 : 1;
|
|
73
|
-
for (const raw of rawRecords) {
|
|
74
|
-
const normalized = normalizeRecord(raw);
|
|
75
|
-
if (normalized) records.push(normalized);
|
|
76
|
-
else invalidSkipped += 1;
|
|
77
|
-
}
|
|
78
|
-
return { records, invalidSkipped };
|
|
62
|
+
return readRecordStore(paths.memoryV2RecordsPath);
|
|
79
63
|
}
|
|
80
64
|
|
|
81
65
|
/**
|
|
@@ -83,13 +67,7 @@ async function readStore(projectRoot) {
|
|
|
83
67
|
*/
|
|
84
68
|
export async function saveRecords(projectRoot, records) {
|
|
85
69
|
const paths = buildRuntimePaths(projectRoot);
|
|
86
|
-
|
|
87
|
-
.map((raw) => normalizeRecord(raw))
|
|
88
|
-
.filter(Boolean);
|
|
89
|
-
await writeJson(paths.memoryV2RecordsPath, {
|
|
90
|
-
schemaVersion: SCHEMA_VERSION,
|
|
91
|
-
records: normalized,
|
|
92
|
-
});
|
|
70
|
+
await writeRecordStore(paths.memoryV2RecordsPath, records);
|
|
93
71
|
}
|
|
94
72
|
|
|
95
73
|
/**
|
|
@@ -108,31 +86,17 @@ export async function addRecord(projectRoot, recordInput) {
|
|
|
108
86
|
return record;
|
|
109
87
|
}
|
|
110
88
|
|
|
111
|
-
const PATCHABLE_FIELDS = ['type', 'scope', 'text', 'provenance', 'confidence',
|
|
112
|
-
'valid_until', 'status', 'project_id', 'meta', 'source_fingerprint'];
|
|
113
|
-
|
|
114
89
|
/**
|
|
115
90
|
* updateRecord(projectRoot, id, patch) → record | null
|
|
116
91
|
*/
|
|
117
92
|
export async function updateRecord(projectRoot, id, patch = {}) {
|
|
118
93
|
await ensureMigrated(projectRoot);
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (Object.prototype.hasOwnProperty.call(patch, field)) {
|
|
126
|
-
next[field] = patch[field];
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const normalized = normalizeRecord(next);
|
|
130
|
-
if (!normalized) {
|
|
131
|
-
throw new Error(`updateRecord: patch produces an invalid record (id ${id})`);
|
|
132
|
-
}
|
|
133
|
-
records[index] = normalized;
|
|
134
|
-
await saveRecords(projectRoot, records);
|
|
135
|
-
return normalized;
|
|
94
|
+
const paths = buildRuntimePaths(projectRoot);
|
|
95
|
+
const { records } = await readRecordStore(paths.memoryV2RecordsPath);
|
|
96
|
+
const result = applyRecordPatch(records, id, patch);
|
|
97
|
+
if (!result) return null;
|
|
98
|
+
await writeRecordStore(paths.memoryV2RecordsPath, result.records);
|
|
99
|
+
return result.record;
|
|
136
100
|
}
|
|
137
101
|
|
|
138
102
|
/**
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// User-scope memory store — SPEC FR-008.
|
|
2
|
+
// Canonical user-level memory API at ~/.ukit/storage/memory/v2/records.json,
|
|
3
|
+
// shared across projects. Same v2 record document as the project store but
|
|
4
|
+
// with NO v1→v2 migration (the user layer starts at v2). All functions take
|
|
5
|
+
// `{homeDir}={}` and resolve paths via buildUserPaths.
|
|
6
|
+
|
|
7
|
+
import { buildUserPaths } from '../userPaths.js';
|
|
8
|
+
import { createRecord, normalizeRecord } from './records.js';
|
|
9
|
+
import {
|
|
10
|
+
readRecordStore,
|
|
11
|
+
writeRecordStore,
|
|
12
|
+
applyRecordPatch,
|
|
13
|
+
isRawRecord,
|
|
14
|
+
} from './recordStore.js';
|
|
15
|
+
import { loadRecords } from './storeV2.js';
|
|
16
|
+
|
|
17
|
+
function userRecordsPath(homeDir) {
|
|
18
|
+
return buildUserPaths({ homeDir }).memoryV2RecordsPath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* loadUserRecords({homeDir}={}) → record[] — tolerant: invalid entries skipped.
|
|
23
|
+
*/
|
|
24
|
+
export async function loadUserRecords({ homeDir } = {}) {
|
|
25
|
+
const { records } = await readRecordStore(userRecordsPath(homeDir));
|
|
26
|
+
return records;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* saveUserRecords(records, {homeDir}={}) → void — atomic write.
|
|
31
|
+
*/
|
|
32
|
+
export async function saveUserRecords(records, { homeDir } = {}) {
|
|
33
|
+
await writeRecordStore(userRecordsPath(homeDir), records);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* addUserRecord(recordInput, {homeDir}={}) → record
|
|
38
|
+
* Accepts either createRecord-style input or a full raw record (normalized).
|
|
39
|
+
*/
|
|
40
|
+
export async function addUserRecord(recordInput, { homeDir } = {}) {
|
|
41
|
+
const record = isRawRecord(recordInput)
|
|
42
|
+
? normalizeRecord(recordInput)
|
|
43
|
+
: createRecord(recordInput);
|
|
44
|
+
if (!record) {
|
|
45
|
+
throw new Error('addUserRecord: input could not be normalized into a valid record');
|
|
46
|
+
}
|
|
47
|
+
const recordsPath = userRecordsPath(homeDir);
|
|
48
|
+
const { records } = await readRecordStore(recordsPath);
|
|
49
|
+
await writeRecordStore(recordsPath, [...records, record]);
|
|
50
|
+
return record;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* updateUserRecord(id, patch, {homeDir}={}) → record | null
|
|
55
|
+
*/
|
|
56
|
+
export async function updateUserRecord(id, patch = {}, { homeDir } = {}) {
|
|
57
|
+
const recordsPath = userRecordsPath(homeDir);
|
|
58
|
+
const { records } = await readRecordStore(recordsPath);
|
|
59
|
+
const result = applyRecordPatch(records, id, patch);
|
|
60
|
+
if (!result) return null;
|
|
61
|
+
await writeRecordStore(recordsPath, result.records);
|
|
62
|
+
return result.record;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* getUserRecord(id, {homeDir}={}) → record | null
|
|
67
|
+
*/
|
|
68
|
+
export async function getUserRecord(id, { homeDir } = {}) {
|
|
69
|
+
const { records } = await readRecordStore(userRecordsPath(homeDir));
|
|
70
|
+
return records.find((r) => r.id === id) ?? null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* queryUserRecords({ type?, scope?, status?, projectId? }, {homeDir}={}) → record[]
|
|
75
|
+
*/
|
|
76
|
+
export async function queryUserRecords({ type, scope, status, projectId } = {}, { homeDir } = {}) {
|
|
77
|
+
const records = await loadUserRecords({ homeDir });
|
|
78
|
+
return records.filter((r) => (type == null || r.type === type)
|
|
79
|
+
&& (scope == null || r.scope === scope)
|
|
80
|
+
&& (status == null || r.status === status)
|
|
81
|
+
&& (projectId == null || r.project_id === projectId));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* userMemoryStats({homeDir}={}) → { total, byType, byStatus, invalidSkipped }
|
|
86
|
+
*/
|
|
87
|
+
export async function userMemoryStats({ homeDir } = {}) {
|
|
88
|
+
const { records, invalidSkipped } = await readRecordStore(userRecordsPath(homeDir));
|
|
89
|
+
const byType = {};
|
|
90
|
+
const byStatus = {};
|
|
91
|
+
for (const r of records) {
|
|
92
|
+
byType[r.type] = (byType[r.type] ?? 0) + 1;
|
|
93
|
+
byStatus[r.status] = (byStatus[r.status] ?? 0) + 1;
|
|
94
|
+
}
|
|
95
|
+
return { total: records.length, byType, byStatus, invalidSkipped };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* loadMergedRecords(projectRoot, {homeDir}={}) → record[]
|
|
100
|
+
* Project records ++ user records; the user record wins on id collision.
|
|
101
|
+
*/
|
|
102
|
+
export async function loadMergedRecords(projectRoot, { homeDir } = {}) {
|
|
103
|
+
const [projectRecords, userRecords] = await Promise.all([
|
|
104
|
+
loadRecords(projectRoot),
|
|
105
|
+
loadUserRecords({ homeDir }),
|
|
106
|
+
]);
|
|
107
|
+
const merged = new Map();
|
|
108
|
+
for (const r of projectRecords) merged.set(r.id, r);
|
|
109
|
+
for (const r of userRecords) merged.set(r.id, r);
|
|
110
|
+
return [...merged.values()];
|
|
111
|
+
}
|
package/src/core/paths.js
CHANGED
|
@@ -5,6 +5,7 @@ export function buildPathConfig({ packageRoot, projectRoot }) {
|
|
|
5
5
|
packageRoot,
|
|
6
6
|
projectRoot,
|
|
7
7
|
manifestPath: path.join(packageRoot, 'manifests', 'platform.full.yaml'),
|
|
8
|
+
userManifestPath: path.join(packageRoot, 'manifests', 'platform.user.yaml'),
|
|
8
9
|
templatesRoot: path.join(packageRoot, 'templates'),
|
|
9
10
|
ukitRoot: path.join(projectRoot, '.claude', 'ukit', '.ukit'),
|
|
10
11
|
installMetaPath: path.join(projectRoot, '.claude', 'ukit', '.ukit', 'install.json'),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { loadManifest } from '../manifest/loadManifest.js';
|
|
4
5
|
import { detectStack } from '../stack/detectStack.js';
|
|
@@ -14,9 +15,10 @@ import { cleanupLegacyPaths, migrateLegacyRuntimeRoot } from './migrateLegacy.js
|
|
|
14
15
|
import { ensureGitignore } from './ensureGitignore.js';
|
|
15
16
|
import { repairBrokenHooks } from './repairBrokenHooks.js';
|
|
16
17
|
import { applyGatewayResilienceEnv } from './gatewayResilienceEnv.js';
|
|
17
|
-
import { cleanupEmptyParents, readJsonIfExists, removeFileOrLinkOnly, resolveProjectRelativePath } from './fileOps.js';
|
|
18
|
+
import { cleanupEmptyParents, readJsonIfExists, removeFileOrLinkOnly, resolveProjectRelativePath, writeJson } from './fileOps.js';
|
|
18
19
|
import { findOpencodeArtifacts, opencodeSteerMessage } from './opencodeSteer.js';
|
|
19
20
|
import { loadRuntimeConfig } from './runtimeConfig.js';
|
|
21
|
+
import { buildUserPaths } from './userPaths.js';
|
|
20
22
|
|
|
21
23
|
const AUTO_PRUNE_OBSOLETE_PREFIXES = [
|
|
22
24
|
'.claude/skills/',
|
|
@@ -247,6 +249,74 @@ async function pruneObsoleteManagedPaths({
|
|
|
247
249
|
|
|
248
250
|
return removedCount;
|
|
249
251
|
}
|
|
252
|
+
// SPEC FR-004 — user-layer seeding pass. Runs after the project plan/diff (and
|
|
253
|
+
// after the project apply on real installs): seeds ~/.ukit/ from
|
|
254
|
+
// manifests/platform.user.yaml. Every user item is `mergeStrategy: skip`, so
|
|
255
|
+
// existing user files are never touched — install only creates what is missing.
|
|
256
|
+
// The pass is advisory: any failure is caught, warned, and reported via
|
|
257
|
+
// `userLayer.error` — it must never fail the project install.
|
|
258
|
+
// User-layer writes are deliberately NOT fed to writeInstallMetadata or
|
|
259
|
+
// buildManagedRelativePathSet — those are project-scoped; the user layer has its
|
|
260
|
+
// own marker at ~/.ukit/install.json.
|
|
261
|
+
async function runUserLayerPass({
|
|
262
|
+
pathConfig,
|
|
263
|
+
homeDir,
|
|
264
|
+
stackContext,
|
|
265
|
+
variables,
|
|
266
|
+
selectedAdapterItemIds,
|
|
267
|
+
packageVersion,
|
|
268
|
+
dryRun,
|
|
269
|
+
}) {
|
|
270
|
+
const userLayer = { seeded: 0, kept: 0, skipped: 0, error: null, userRoot: null };
|
|
271
|
+
let userRows = [];
|
|
272
|
+
try {
|
|
273
|
+
// buildUserPaths lives inside the try so a bad homeDir can never fail the
|
|
274
|
+
// project install — the whole pass is advisory.
|
|
275
|
+
const userPaths = buildUserPaths({ homeDir });
|
|
276
|
+
userLayer.userRoot = userPaths.userRoot;
|
|
277
|
+
// pathConfig.userManifestPath is set by buildPathConfig; fall back to the
|
|
278
|
+
// package layout (templatesRoot's sibling manifests/) for callers that build
|
|
279
|
+
// pathConfig by hand (tests, embedded runners).
|
|
280
|
+
const userManifestPath = pathConfig.userManifestPath
|
|
281
|
+
?? path.join(pathConfig.templatesRoot, '..', 'manifests', 'platform.user.yaml');
|
|
282
|
+
const userManifest = await loadManifest(userManifestPath);
|
|
283
|
+
const userPlan = await buildInstallPlan({
|
|
284
|
+
manifest: userManifest,
|
|
285
|
+
stackContext,
|
|
286
|
+
templatesRoot: pathConfig.templatesRoot,
|
|
287
|
+
variables,
|
|
288
|
+
projectRoot: userPaths.userRoot,
|
|
289
|
+
selectedAdapterItemIds,
|
|
290
|
+
});
|
|
291
|
+
const userDiff = await diffInstallPlan(userPlan);
|
|
292
|
+
const userSummary = summarizeDiff(userDiff);
|
|
293
|
+
userRows = toDiffRows(userDiff).map((row) => ({ ...row, layer: 'user' }));
|
|
294
|
+
|
|
295
|
+
if (!dryRun) {
|
|
296
|
+
const { skippedByAction } = await applyDiffResults(userDiff, {
|
|
297
|
+
backupRoot: path.join(userPaths.userRoot, 'backups'),
|
|
298
|
+
projectRoot: userPaths.userRoot,
|
|
299
|
+
});
|
|
300
|
+
userLayer.seeded = userSummary.create - (skippedByAction?.create ?? 0);
|
|
301
|
+
userLayer.kept = userSummary.skip + userSummary.unchanged;
|
|
302
|
+
userLayer.skipped = (skippedByAction?.create ?? 0) + (skippedByAction?.update ?? 0);
|
|
303
|
+
// UKit-managed marker (not user data): records that the pass ran.
|
|
304
|
+
await writeJson(userPaths.installMetaPath, {
|
|
305
|
+
tool: 'ukit',
|
|
306
|
+
version: packageVersion,
|
|
307
|
+
seededAt: new Date().toISOString(),
|
|
308
|
+
});
|
|
309
|
+
} else {
|
|
310
|
+
userLayer.seeded = userSummary.create;
|
|
311
|
+
userLayer.kept = userSummary.skip + userSummary.unchanged;
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
userLayer.error = error?.message ?? String(error);
|
|
315
|
+
console.warn(`[UKit] Warning: user-layer seeding skipped — ${userLayer.error}`);
|
|
316
|
+
}
|
|
317
|
+
return { userLayer, userRows };
|
|
318
|
+
}
|
|
319
|
+
|
|
250
320
|
|
|
251
321
|
export async function runInstallPipeline({
|
|
252
322
|
packageVersion,
|
|
@@ -255,6 +325,7 @@ export async function runInstallPipeline({
|
|
|
255
325
|
selectedAdapterItemIds,
|
|
256
326
|
retainedManagedRelativePaths = [],
|
|
257
327
|
withCodegraph = false,
|
|
328
|
+
homeDir = os.homedir(),
|
|
258
329
|
}) {
|
|
259
330
|
// Check for an existing install before making any changes.
|
|
260
331
|
// cleanupLegacyPaths() is only safe to run on reinstalls: on a fresh install
|
|
@@ -404,28 +475,50 @@ export async function runInstallPipeline({
|
|
|
404
475
|
retainedManagedRelativePaths,
|
|
405
476
|
});
|
|
406
477
|
|
|
478
|
+
const { userLayer, userRows } = await runUserLayerPass({
|
|
479
|
+
pathConfig,
|
|
480
|
+
homeDir,
|
|
481
|
+
stackContext,
|
|
482
|
+
variables,
|
|
483
|
+
selectedAdapterItemIds,
|
|
484
|
+
packageVersion,
|
|
485
|
+
dryRun: false,
|
|
486
|
+
});
|
|
487
|
+
|
|
407
488
|
return {
|
|
408
489
|
manifest,
|
|
409
490
|
stackContext,
|
|
410
491
|
projectContext,
|
|
411
492
|
providerContext,
|
|
412
493
|
summary,
|
|
413
|
-
rows: toDiffRows(diffResults),
|
|
494
|
+
rows: [...toDiffRows(diffResults), ...userRows],
|
|
414
495
|
writes,
|
|
415
496
|
hookRepair,
|
|
416
497
|
gatewayResilience,
|
|
498
|
+
userLayer,
|
|
417
499
|
};
|
|
418
500
|
}
|
|
419
501
|
|
|
502
|
+
const { userLayer, userRows } = await runUserLayerPass({
|
|
503
|
+
pathConfig,
|
|
504
|
+
homeDir,
|
|
505
|
+
stackContext,
|
|
506
|
+
variables,
|
|
507
|
+
selectedAdapterItemIds,
|
|
508
|
+
packageVersion,
|
|
509
|
+
dryRun: true,
|
|
510
|
+
});
|
|
511
|
+
|
|
420
512
|
return {
|
|
421
513
|
manifest,
|
|
422
514
|
stackContext,
|
|
423
515
|
projectContext,
|
|
424
516
|
providerContext,
|
|
425
517
|
summary: plannedSummary,
|
|
426
|
-
rows: toDiffRows(diffResults),
|
|
518
|
+
rows: [...toDiffRows(diffResults), ...userRows],
|
|
427
519
|
writes: [],
|
|
428
520
|
hookRepair: { removals: [], files: [] },
|
|
429
521
|
gatewayResilience: { customGateway: false, reason: 'dry-run' },
|
|
522
|
+
userLayer,
|
|
430
523
|
};
|
|
431
524
|
}
|