threadnote 1.7.0 → 1.7.1
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/config/post-update-migrations.json +5 -4
- package/dist/threadnote.cjs +223 -33
- package/package.json +1 -1
|
@@ -25,15 +25,16 @@
|
|
|
25
25
|
"title": "Consolidate memories under the git remote project name",
|
|
26
26
|
"description": [
|
|
27
27
|
"Threadnote now uses the git remote repository name as the default project identifier instead of the clone folder name.",
|
|
28
|
-
"This migration scans personal lifecycle-aware memory folders
|
|
29
|
-
"It
|
|
28
|
+
"This migration scans personal lifecycle-aware memory project folders and uses repo_path evidence in memories, plus matching seed manifest paths when present, to resolve git remote names.",
|
|
29
|
+
"It covers every detected personal memory project, not just the workspace where threadnote update was run.",
|
|
30
|
+
"It also renames matching seed manifest projects and workset references so future seeded guidance uses the remote-derived project name.",
|
|
30
31
|
"Matching memories are copied under the remote-derived project name with updated project metadata, then the old copy is removed.",
|
|
31
32
|
"If a destination file already exists with different content, Threadnote keeps both by writing the migrated copy with a -from-<old-project> suffix.",
|
|
32
|
-
"Shared team memories and
|
|
33
|
+
"Shared team memories and projects without repo path evidence are left untouched."
|
|
33
34
|
],
|
|
34
35
|
"commandArgs": ["migrate-project-names", "--apply"],
|
|
35
36
|
"instructions": [
|
|
36
|
-
"Post-update project-name migration finished. Future default handoffs and memories for
|
|
37
|
+
"Post-update project-name migration finished. Future default handoffs and memories for migrated repos will use the git remote repository name as their project.",
|
|
37
38
|
"If the migration updated your seed manifest, run threadnote seed --only <new-project-name> to re-ingest seeded resources under the new project URI.",
|
|
38
39
|
"If Threadnote reported any original files were still being processed, rerun the printed threadnote forget <uri> command later."
|
|
39
40
|
],
|
package/dist/threadnote.cjs
CHANGED
|
@@ -11633,21 +11633,30 @@ async function runMigrateLifecycle(config, options) {
|
|
|
11633
11633
|
async function runMigrateProjectNames(config, options) {
|
|
11634
11634
|
const dryRun = options.dryRun === true || options.apply !== true;
|
|
11635
11635
|
const limit = options.limit ? parsePositiveInteger(options.limit, "project-name migration limit") : void 0;
|
|
11636
|
-
const
|
|
11637
|
-
if (
|
|
11638
|
-
console.log("No git remote project-name
|
|
11636
|
+
const contexts = await projectNameMigrationContexts(config);
|
|
11637
|
+
if (contexts.length === 0) {
|
|
11638
|
+
console.log("No git remote project-name changes apply across configured projects.");
|
|
11639
11639
|
return;
|
|
11640
11640
|
}
|
|
11641
|
-
const
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11641
|
+
const plans = [];
|
|
11642
|
+
let remaining = limit;
|
|
11643
|
+
for (const context of contexts) {
|
|
11644
|
+
const candidates2 = remaining === 0 ? [] : await projectNameMigrationCandidates(config, context, remaining);
|
|
11645
|
+
plans.push({ candidates: candidates2, context });
|
|
11646
|
+
if (remaining !== void 0) {
|
|
11647
|
+
remaining = Math.max(0, remaining - candidates2.length);
|
|
11648
|
+
}
|
|
11649
|
+
}
|
|
11650
|
+
const seedManifestMigration = await seedManifestProjectNameMigration(config, contexts);
|
|
11651
|
+
if (!plans.some((plan) => plan.candidates.length > 0) && !seedManifestMigration) {
|
|
11652
|
+
console.log("No project-name migration candidates found across configured projects.");
|
|
11645
11653
|
return;
|
|
11646
11654
|
}
|
|
11647
|
-
const seedManifestUpdated = await
|
|
11655
|
+
const seedManifestUpdated = await migrateSeedManifestProjectNames(config, seedManifestMigration, dryRun);
|
|
11648
11656
|
let existingCount = 0;
|
|
11649
11657
|
let migratedCount = 0;
|
|
11650
11658
|
let skippedCount = 0;
|
|
11659
|
+
const candidates = plans.flatMap((plan) => [...plan.candidates]);
|
|
11651
11660
|
if (candidates.length > 0) {
|
|
11652
11661
|
const ov = await openVikingCliForMode(dryRun);
|
|
11653
11662
|
for (const candidate of candidates) {
|
|
@@ -11671,37 +11680,186 @@ async function runMigrateProjectNames(config, options) {
|
|
|
11671
11680
|
migratedCount += 1;
|
|
11672
11681
|
}
|
|
11673
11682
|
}
|
|
11683
|
+
const activeContexts = projectNameMigrationActiveContexts(plans, seedManifestMigration);
|
|
11684
|
+
const newProjectsToSeed = [...new Set(seedManifestMigration?.newProjects ?? [])];
|
|
11674
11685
|
console.log(
|
|
11675
11686
|
[
|
|
11676
|
-
|
|
11687
|
+
projectNameMigrationSummary(migratedCount, dryRun, activeContexts),
|
|
11677
11688
|
seedManifestUpdated ? `seed manifest ${dryRun ? "would be updated" : "updated"}` : "seed manifest unchanged",
|
|
11678
11689
|
`${existingCount} duplicate destination(s) reused`,
|
|
11679
11690
|
`${skippedCount} source(s) still processing`,
|
|
11680
11691
|
dryRun ? "Run with --apply to perform this migration." : void 0,
|
|
11681
|
-
|
|
11692
|
+
...newProjectsToSeed.map(
|
|
11693
|
+
(project) => `Run threadnote seed --only ${project} to re-ingest seeded resources under the new project URI.`
|
|
11694
|
+
)
|
|
11682
11695
|
].filter((part) => part !== void 0).join("; ")
|
|
11683
11696
|
);
|
|
11684
11697
|
}
|
|
11685
11698
|
async function hasProjectNameMigrationCandidates(config) {
|
|
11686
|
-
const
|
|
11687
|
-
|
|
11699
|
+
const contexts = await projectNameMigrationContexts(config);
|
|
11700
|
+
if (contexts.length === 0) {
|
|
11701
|
+
return false;
|
|
11702
|
+
}
|
|
11703
|
+
for (const context of contexts) {
|
|
11704
|
+
if ((await projectNameMigrationCandidates(config, context, 1)).length > 0) {
|
|
11705
|
+
return true;
|
|
11706
|
+
}
|
|
11707
|
+
}
|
|
11708
|
+
return await seedManifestProjectNameMigration(config, contexts) !== void 0;
|
|
11709
|
+
}
|
|
11710
|
+
async function projectNameMigrationContexts(config) {
|
|
11711
|
+
const evidence = await projectNameMigrationMemoryEvidence(config);
|
|
11712
|
+
const contexts = [];
|
|
11713
|
+
let manifest;
|
|
11714
|
+
try {
|
|
11715
|
+
manifest = await readSeedManifest(config.manifestPath);
|
|
11716
|
+
} catch (_err) {
|
|
11717
|
+
manifest = void 0;
|
|
11718
|
+
}
|
|
11719
|
+
if (manifest) {
|
|
11720
|
+
for (const project of manifest.projects) {
|
|
11721
|
+
const projectEvidence = evidence.get(uriSegment(project.name));
|
|
11722
|
+
if (projectEvidence) {
|
|
11723
|
+
projectEvidence.repoPaths.add(expandPath(project.path));
|
|
11724
|
+
}
|
|
11725
|
+
}
|
|
11726
|
+
}
|
|
11727
|
+
for (const projectEvidence of evidence.values()) {
|
|
11728
|
+
for (const repoPath of projectEvidence.repoPaths) {
|
|
11729
|
+
const context = await projectNameMigrationContextForRepoPath(projectEvidence.oldProject, repoPath);
|
|
11730
|
+
if (context) {
|
|
11731
|
+
contexts.push(context);
|
|
11732
|
+
}
|
|
11733
|
+
}
|
|
11734
|
+
}
|
|
11735
|
+
const currentContext = await currentWorkspaceProjectNameMigrationContext(evidence);
|
|
11736
|
+
if (currentContext) {
|
|
11737
|
+
contexts.push(currentContext);
|
|
11738
|
+
}
|
|
11739
|
+
return dedupeProjectNameMigrationContexts(contexts);
|
|
11740
|
+
}
|
|
11741
|
+
async function projectNameMigrationMemoryEvidence(config) {
|
|
11742
|
+
const evidence = /* @__PURE__ */ new Map();
|
|
11743
|
+
for (const location of projectMemoryLocations()) {
|
|
11744
|
+
const locationRoot = (0, import_node_path8.join)(localUserMemoriesRoot(config), ...location.relativePath);
|
|
11745
|
+
let projectEntries;
|
|
11746
|
+
try {
|
|
11747
|
+
projectEntries = await (0, import_promises6.readdir)(locationRoot, { withFileTypes: true });
|
|
11748
|
+
} catch (_err) {
|
|
11749
|
+
continue;
|
|
11750
|
+
}
|
|
11751
|
+
for (const projectEntry of projectEntries) {
|
|
11752
|
+
if (!projectEntry.isDirectory() || projectEntry.name.startsWith(".")) {
|
|
11753
|
+
continue;
|
|
11754
|
+
}
|
|
11755
|
+
const oldSegment = projectEntry.name;
|
|
11756
|
+
const projectEvidence = ensureProjectNameMigrationEvidence(evidence, oldSegment);
|
|
11757
|
+
const projectDirectory = (0, import_node_path8.join)(locationRoot, projectEntry.name);
|
|
11758
|
+
let memoryEntries;
|
|
11759
|
+
try {
|
|
11760
|
+
memoryEntries = await (0, import_promises6.readdir)(projectDirectory, { withFileTypes: true });
|
|
11761
|
+
} catch (_err) {
|
|
11762
|
+
continue;
|
|
11763
|
+
}
|
|
11764
|
+
for (const memoryEntry of memoryEntries) {
|
|
11765
|
+
if (!memoryEntry.isFile() || memoryEntry.name.startsWith(".") || !memoryEntry.name.endsWith(".md")) {
|
|
11766
|
+
continue;
|
|
11767
|
+
}
|
|
11768
|
+
const content = await readTextIfExists((0, import_node_path8.join)(projectDirectory, memoryEntry.name));
|
|
11769
|
+
if (!content) {
|
|
11770
|
+
continue;
|
|
11771
|
+
}
|
|
11772
|
+
const sourceUri = `viking://user/${uriSegment(config.user)}/memories/${location.uriPath}/${oldSegment}/${memoryEntry.name}`;
|
|
11773
|
+
const record = parseMemoryDocument(sourceUri, content);
|
|
11774
|
+
if (record?.metadata.project && uriSegment(record.metadata.project) === oldSegment) {
|
|
11775
|
+
projectEvidence.oldProject = record.metadata.project;
|
|
11776
|
+
}
|
|
11777
|
+
const repoPath = repoPathEvidenceFromMemory(content);
|
|
11778
|
+
if (repoPath) {
|
|
11779
|
+
projectEvidence.repoPaths.add(repoPath);
|
|
11780
|
+
}
|
|
11781
|
+
}
|
|
11782
|
+
}
|
|
11783
|
+
}
|
|
11784
|
+
return evidence;
|
|
11785
|
+
}
|
|
11786
|
+
function ensureProjectNameMigrationEvidence(evidence, oldSegment) {
|
|
11787
|
+
const existing = evidence.get(oldSegment);
|
|
11788
|
+
if (existing) {
|
|
11789
|
+
return existing;
|
|
11790
|
+
}
|
|
11791
|
+
const created = { oldProject: oldSegment, oldSegment, repoPaths: /* @__PURE__ */ new Set() };
|
|
11792
|
+
evidence.set(oldSegment, created);
|
|
11793
|
+
return created;
|
|
11794
|
+
}
|
|
11795
|
+
function repoPathEvidenceFromMemory(content) {
|
|
11796
|
+
const match = /^repo_path:\s*(.+)$/m.exec(content);
|
|
11797
|
+
if (!match?.[1]) {
|
|
11798
|
+
return void 0;
|
|
11799
|
+
}
|
|
11800
|
+
const cleaned = match[1].trim().replace(/^['"`]+|['"`]+$/g, "").replace(/[.,;]+$/g, "");
|
|
11801
|
+
if (!cleaned.startsWith("/") && !cleaned.startsWith("~/")) {
|
|
11802
|
+
return void 0;
|
|
11803
|
+
}
|
|
11804
|
+
return expandPath(cleaned);
|
|
11688
11805
|
}
|
|
11689
|
-
async function
|
|
11806
|
+
async function projectNameMigrationContextForRepoPath(oldProject, repoPath) {
|
|
11807
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], repoPath);
|
|
11808
|
+
if (!repoRoot) {
|
|
11809
|
+
return void 0;
|
|
11810
|
+
}
|
|
11811
|
+
const newProject = await resolveGitRemoteRepoName(repoRoot);
|
|
11812
|
+
if (!newProject) {
|
|
11813
|
+
return void 0;
|
|
11814
|
+
}
|
|
11815
|
+
return projectNameMigrationContextFromParts({
|
|
11816
|
+
newProject,
|
|
11817
|
+
oldProject,
|
|
11818
|
+
repoRoot
|
|
11819
|
+
});
|
|
11820
|
+
}
|
|
11821
|
+
async function currentWorkspaceProjectNameMigrationContext(evidence) {
|
|
11690
11822
|
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
11691
11823
|
if (!repoRoot) {
|
|
11692
11824
|
return void 0;
|
|
11693
11825
|
}
|
|
11694
|
-
const newProject = await
|
|
11826
|
+
const newProject = await resolveGitRemoteRepoName(repoRoot);
|
|
11695
11827
|
const oldProject = await resolveRepoFolderName(repoRoot);
|
|
11696
11828
|
if (!newProject || !oldProject) {
|
|
11697
11829
|
return void 0;
|
|
11698
11830
|
}
|
|
11699
|
-
const newSegment = uriSegment(newProject);
|
|
11700
11831
|
const oldSegment = uriSegment(oldProject);
|
|
11832
|
+
if (!evidence.has(oldSegment)) {
|
|
11833
|
+
return void 0;
|
|
11834
|
+
}
|
|
11835
|
+
return projectNameMigrationContextFromParts({ newProject, oldProject, repoRoot });
|
|
11836
|
+
}
|
|
11837
|
+
function projectNameMigrationContextFromParts(params) {
|
|
11838
|
+
const newSegment = uriSegment(params.newProject);
|
|
11839
|
+
const oldSegment = uriSegment(params.oldProject);
|
|
11701
11840
|
if (newSegment === oldSegment) {
|
|
11702
11841
|
return void 0;
|
|
11703
11842
|
}
|
|
11704
|
-
return {
|
|
11843
|
+
return {
|
|
11844
|
+
newProject: params.newProject,
|
|
11845
|
+
newSegment,
|
|
11846
|
+
oldProject: params.oldProject,
|
|
11847
|
+
oldSegment,
|
|
11848
|
+
repoRoot: params.repoRoot
|
|
11849
|
+
};
|
|
11850
|
+
}
|
|
11851
|
+
function dedupeProjectNameMigrationContexts(contexts) {
|
|
11852
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11853
|
+
const out = [];
|
|
11854
|
+
for (const context of contexts) {
|
|
11855
|
+
const key = `${context.oldSegment}\0${context.newSegment}\0${context.repoRoot}`;
|
|
11856
|
+
if (seen.has(key)) {
|
|
11857
|
+
continue;
|
|
11858
|
+
}
|
|
11859
|
+
seen.add(key);
|
|
11860
|
+
out.push(context);
|
|
11861
|
+
}
|
|
11862
|
+
return out;
|
|
11705
11863
|
}
|
|
11706
11864
|
async function projectNameMigrationCandidates(config, context, limit) {
|
|
11707
11865
|
const candidates = [];
|
|
@@ -11753,11 +11911,23 @@ async function projectNameMigrationCandidates(config, context, limit) {
|
|
|
11753
11911
|
}
|
|
11754
11912
|
return candidates;
|
|
11755
11913
|
}
|
|
11756
|
-
|
|
11757
|
-
return
|
|
11914
|
+
function projectNameMigrationActiveContexts(plans, seedManifestMigration) {
|
|
11915
|
+
return dedupeProjectNameMigrationContexts([
|
|
11916
|
+
...plans.filter((plan) => plan.candidates.length > 0).map((plan) => plan.context),
|
|
11917
|
+
...seedManifestMigration?.contexts ?? []
|
|
11918
|
+
]);
|
|
11758
11919
|
}
|
|
11759
|
-
|
|
11760
|
-
const
|
|
11920
|
+
function projectNameMigrationSummary(migratedCount, dryRun, contexts) {
|
|
11921
|
+
const memoryWord = migratedCount === 1 ? "memory" : "memories";
|
|
11922
|
+
const verb = dryRun ? "would be migrated" : "migrated";
|
|
11923
|
+
if (contexts.length === 1) {
|
|
11924
|
+
const [context] = contexts;
|
|
11925
|
+
return `Project-name migration summary: ${migratedCount} ${memoryWord} ${verb} from ${context.oldProject} to ${context.newProject}`;
|
|
11926
|
+
}
|
|
11927
|
+
const renameSummary = contexts.map((context) => `${context.oldProject} -> ${context.newProject}`).join(", ");
|
|
11928
|
+
return `Project-name migration summary: ${migratedCount} ${memoryWord} ${verb} across ${contexts.length} project rename(s)${renameSummary ? `: ${renameSummary}` : ""}`;
|
|
11929
|
+
}
|
|
11930
|
+
async function migrateSeedManifestProjectNames(config, migration, dryRun) {
|
|
11761
11931
|
if (!migration) {
|
|
11762
11932
|
return false;
|
|
11763
11933
|
}
|
|
@@ -11779,33 +11949,45 @@ async function migrateSeedManifestProjectName(config, context, dryRun) {
|
|
|
11779
11949
|
console.log(`Updated seed manifest: ${config.manifestPath}`);
|
|
11780
11950
|
return true;
|
|
11781
11951
|
}
|
|
11782
|
-
async function seedManifestProjectNameMigration(config,
|
|
11952
|
+
async function seedManifestProjectNameMigration(config, contexts) {
|
|
11783
11953
|
let manifest;
|
|
11784
11954
|
try {
|
|
11785
11955
|
manifest = await readSeedManifest(config.manifestPath);
|
|
11786
11956
|
} catch (_err) {
|
|
11787
11957
|
return void 0;
|
|
11788
11958
|
}
|
|
11789
|
-
const
|
|
11790
|
-
const newDefaultUri = `viking://resources/repos/${context.newSegment}`;
|
|
11791
|
-
const newNameExists = manifest.projects.some((project) => uriSegment(project.name) === context.newSegment);
|
|
11959
|
+
const renamed = /* @__PURE__ */ new Map();
|
|
11792
11960
|
let changed = false;
|
|
11793
|
-
let renamedProject = false;
|
|
11794
11961
|
const projects = manifest.projects.map((project) => {
|
|
11795
|
-
|
|
11962
|
+
const context = contexts.find(
|
|
11963
|
+
(candidate) => isSeedManifestProjectNameCandidate(
|
|
11964
|
+
project,
|
|
11965
|
+
candidate,
|
|
11966
|
+
`viking://resources/repos/${candidate.oldSegment}`,
|
|
11967
|
+
`viking://resources/repos/${candidate.newSegment}`
|
|
11968
|
+
)
|
|
11969
|
+
);
|
|
11970
|
+
if (!context) {
|
|
11971
|
+
return project;
|
|
11972
|
+
}
|
|
11973
|
+
const newNameExists = manifest.projects.some(
|
|
11974
|
+
(other) => other !== project && uriSegment(other.name) === context.newSegment
|
|
11975
|
+
);
|
|
11976
|
+
if (newNameExists || [...renamed.values()].some((existing) => existing.newSegment === context.newSegment)) {
|
|
11796
11977
|
return project;
|
|
11797
11978
|
}
|
|
11798
11979
|
changed = true;
|
|
11799
|
-
|
|
11980
|
+
renamed.set(context.oldSegment, context);
|
|
11800
11981
|
return {
|
|
11801
11982
|
...project,
|
|
11802
11983
|
name: context.newProject,
|
|
11803
|
-
uri: trimTrailingSlash(project.uri) ===
|
|
11984
|
+
uri: trimTrailingSlash(project.uri) === `viking://resources/repos/${context.oldSegment}` ? `viking://resources/repos/${context.newSegment}` : project.uri
|
|
11804
11985
|
};
|
|
11805
11986
|
});
|
|
11806
|
-
const worksets =
|
|
11987
|
+
const worksets = renamed.size > 0 ? manifest.worksets?.map((workset) => {
|
|
11807
11988
|
const members = workset.projects.map((projectName) => {
|
|
11808
|
-
|
|
11989
|
+
const context = renamed.get(uriSegment(projectName));
|
|
11990
|
+
if (!context) {
|
|
11809
11991
|
return projectName;
|
|
11810
11992
|
}
|
|
11811
11993
|
changed = true;
|
|
@@ -11817,6 +11999,8 @@ async function seedManifestProjectNameMigration(config, context) {
|
|
|
11817
11999
|
return void 0;
|
|
11818
12000
|
}
|
|
11819
12001
|
return {
|
|
12002
|
+
contexts: [...renamed.values()],
|
|
12003
|
+
newProjects: [...new Set([...renamed.values()].map((context) => context.newProject))],
|
|
11820
12004
|
output: `${index_vite_proxy_tmp_default.dump(
|
|
11821
12005
|
{
|
|
11822
12006
|
version: manifest.version,
|
|
@@ -11844,11 +12028,17 @@ async function seedManifestProjectNameMigration(config, context) {
|
|
|
11844
12028
|
)}`
|
|
11845
12029
|
};
|
|
11846
12030
|
}
|
|
11847
|
-
function isSeedManifestProjectNameCandidate(project, context, oldDefaultUri) {
|
|
11848
|
-
|
|
12031
|
+
function isSeedManifestProjectNameCandidate(project, context, oldDefaultUri, newDefaultUri) {
|
|
12032
|
+
const nameSegment = uriSegment(project.name);
|
|
12033
|
+
const uriMatchesOld = trimTrailingSlash(project.uri) === oldDefaultUri;
|
|
12034
|
+
const pathMatchesRepo = expandPath(project.path) === context.repoRoot;
|
|
12035
|
+
if (nameSegment === context.newSegment && !uriMatchesOld) {
|
|
12036
|
+
return false;
|
|
12037
|
+
}
|
|
12038
|
+
if (nameSegment !== context.oldSegment && !uriMatchesOld && !pathMatchesRepo) {
|
|
11849
12039
|
return false;
|
|
11850
12040
|
}
|
|
11851
|
-
return
|
|
12041
|
+
return nameSegment !== context.newSegment || uriMatchesOld || trimTrailingSlash(project.uri) !== newDefaultUri;
|
|
11852
12042
|
}
|
|
11853
12043
|
function canMigrateProjectName(record, context) {
|
|
11854
12044
|
const projectSegment = record.metadata.project ? uriSegment(record.metadata.project) : context.oldSegment;
|