@ddtcorex/dsh-maestro-memory 1.0.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/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +448 -0
- package/cordis.patch.yml +13 -0
- package/lib/client.js +756 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +627 -0
- package/lib/index.js.map +1 -0
- package/lib/memory/batch.d.ts +44 -0
- package/lib/memory/batch.d.ts.map +1 -0
- package/lib/memory/batch.js +75 -0
- package/lib/memory/batch.js.map +1 -0
- package/lib/memory/feedback.d.ts +18 -0
- package/lib/memory/feedback.d.ts.map +1 -0
- package/lib/memory/feedback.js +29 -0
- package/lib/memory/feedback.js.map +1 -0
- package/lib/memory/store.d.ts +154 -0
- package/lib/memory/store.d.ts.map +1 -0
- package/lib/memory/store.js +606 -0
- package/lib/memory/store.js.map +1 -0
- package/lib/migration/cli.d.ts +18 -0
- package/lib/migration/cli.d.ts.map +1 -0
- package/lib/migration/cli.js +143 -0
- package/lib/migration/cli.js.map +1 -0
- package/lib/migration/fixture.d.ts +45 -0
- package/lib/migration/fixture.d.ts.map +1 -0
- package/lib/migration/fixture.js +177 -0
- package/lib/migration/fixture.js.map +1 -0
- package/lib/migration/service.d.ts +75 -0
- package/lib/migration/service.d.ts.map +1 -0
- package/lib/migration/service.js +713 -0
- package/lib/migration/service.js.map +1 -0
- package/lib/prompt/snapshot.d.ts +26 -0
- package/lib/prompt/snapshot.d.ts.map +1 -0
- package/lib/prompt/snapshot.js +74 -0
- package/lib/prompt/snapshot.js.map +1 -0
- package/lib/review/queue.d.ts +72 -0
- package/lib/review/queue.d.ts.map +1 -0
- package/lib/review/queue.js +306 -0
- package/lib/review/queue.js.map +1 -0
- package/lib/skills-browser.d.ts +67 -0
- package/lib/skills-browser.d.ts.map +1 -0
- package/lib/skills-browser.js +185 -0
- package/lib/skills-browser.js.map +1 -0
- package/lib/storage/atomic-store.d.ts +211 -0
- package/lib/storage/atomic-store.d.ts.map +1 -0
- package/lib/storage/atomic-store.js +582 -0
- package/lib/storage/atomic-store.js.map +1 -0
- package/lib/storage/layout.d.ts +81 -0
- package/lib/storage/layout.d.ts.map +1 -0
- package/lib/storage/layout.js +162 -0
- package/lib/storage/layout.js.map +1 -0
- package/lib/storage/legacy-format.d.ts +104 -0
- package/lib/storage/legacy-format.d.ts.map +1 -0
- package/lib/storage/legacy-format.js +262 -0
- package/lib/storage/legacy-format.js.map +1 -0
- package/lib/sync/config.d.ts +22 -0
- package/lib/sync/config.d.ts.map +1 -0
- package/lib/sync/config.js +57 -0
- package/lib/sync/config.js.map +1 -0
- package/lib/sync/git.d.ts +97 -0
- package/lib/sync/git.d.ts.map +1 -0
- package/lib/sync/git.js +282 -0
- package/lib/sync/git.js.map +1 -0
- package/lib/sync/index.d.ts +6 -0
- package/lib/sync/index.d.ts.map +1 -0
- package/lib/sync/index.js +6 -0
- package/lib/sync/index.js.map +1 -0
- package/lib/sync/layout.d.ts +15 -0
- package/lib/sync/layout.d.ts.map +1 -0
- package/lib/sync/layout.js +33 -0
- package/lib/sync/layout.js.map +1 -0
- package/lib/sync/merge.d.ts +42 -0
- package/lib/sync/merge.d.ts.map +1 -0
- package/lib/sync/merge.js +159 -0
- package/lib/sync/merge.js.map +1 -0
- package/lib/sync/service.d.ts +70 -0
- package/lib/sync/service.d.ts.map +1 -0
- package/lib/sync/service.js +508 -0
- package/lib/sync/service.js.map +1 -0
- package/lib/todo/store.d.ts +142 -0
- package/lib/todo/store.d.ts.map +1 -0
- package/lib/todo/store.js +452 -0
- package/lib/todo/store.js.map +1 -0
- package/lib/types/client/index.d.ts +7 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/package.json +63 -0
- package/src/client/index.tsx +994 -0
- package/src/host/index.ts +591 -0
- package/src/host/memory/batch.ts +107 -0
- package/src/host/memory/feedback.ts +36 -0
- package/src/host/memory/store.ts +650 -0
- package/src/host/migration/cli.ts +139 -0
- package/src/host/migration/fixture.ts +218 -0
- package/src/host/migration/service.ts +786 -0
- package/src/host/prompt/snapshot.ts +95 -0
- package/src/host/review/queue.ts +325 -0
- package/src/host/skills-browser.ts +191 -0
- package/src/host/storage/atomic-store.ts +605 -0
- package/src/host/storage/layout.ts +191 -0
- package/src/host/storage/legacy-format.ts +308 -0
- package/src/host/sync/config.ts +67 -0
- package/src/host/sync/git.ts +280 -0
- package/src/host/sync/index.ts +5 -0
- package/src/host/sync/layout.ts +39 -0
- package/src/host/sync/merge.ts +188 -0
- package/src/host/sync/service.ts +494 -0
- package/src/host/todo/store.ts +480 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migration/cli.ts — operations CLI requiring explicit --apply; default is read-only
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { inspect, dryRun, run, verify } from './service.ts'
|
|
6
|
+
import { resolveMemoryRoot } from '../storage/layout.ts'
|
|
7
|
+
|
|
8
|
+
export interface ParsedArgs {
|
|
9
|
+
root: string | null
|
|
10
|
+
apply: boolean
|
|
11
|
+
command: 'inspect' | 'dryRun' | 'run' | 'verify'
|
|
12
|
+
runId?: string
|
|
13
|
+
help: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
17
|
+
const args = argv.slice(2)
|
|
18
|
+
let root: string | null = null
|
|
19
|
+
let apply = false
|
|
20
|
+
let command: ParsedArgs['command'] = 'inspect'
|
|
21
|
+
let runId: string | undefined
|
|
22
|
+
let help = false
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
const a = args[i]
|
|
25
|
+
if (a === '--help' || a === '-h') help = true
|
|
26
|
+
else if (a === '--apply') apply = true
|
|
27
|
+
else if (a === '--inspect') command = 'inspect'
|
|
28
|
+
else if (a === '--dry-run' || a === '--dryRun') command = 'dryRun'
|
|
29
|
+
else if (a === '--verify') command = 'verify'
|
|
30
|
+
else if (a === '--run') command = 'run'
|
|
31
|
+
else if (a === '--root' && i + 1 < args.length) {
|
|
32
|
+
root = args[++i]
|
|
33
|
+
} else if (a.startsWith('--root=')) {
|
|
34
|
+
root = a.slice('--root='.length)
|
|
35
|
+
} else if (a === '--run-id' && i + 1 < args.length) {
|
|
36
|
+
runId = args[++i]
|
|
37
|
+
} else if (a.startsWith('--run-id=')) {
|
|
38
|
+
runId = a.slice('--run-id='.length)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// explicit --apply forces run command unless verify explicitly requested
|
|
42
|
+
if (apply && command === 'inspect') command = 'run'
|
|
43
|
+
// if user asked --verify, keep verify even with --apply? But verify is separate
|
|
44
|
+
// parse loyalty: --verify overrides apply's run conversion
|
|
45
|
+
if (args.includes('--verify')) command = 'verify'
|
|
46
|
+
return { root, apply, command, runId, help }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function helpText(): string {
|
|
50
|
+
return `
|
|
51
|
+
dsh-maestro-memory migration CLI
|
|
52
|
+
|
|
53
|
+
Default is read-only (inspect). Requires explicit --apply to perform backup+adoption.
|
|
54
|
+
|
|
55
|
+
Usage:
|
|
56
|
+
node migrate.mjs [--root <path>] [--inspect|--dry-run|--verify] [--apply] [--run-id <id>]
|
|
57
|
+
|
|
58
|
+
Commands (read-only by default):
|
|
59
|
+
--inspect Read-only inventory, parse, warnings for malformed JSONL/locks/noncanonical (default)
|
|
60
|
+
--dry-run Same as inspect, explicitly read-only (no side effects)
|
|
61
|
+
--verify Verify current files against latest backup manifest (or --run-id); blocks writes on mismatch
|
|
62
|
+
|
|
63
|
+
Write operation (requires --apply):
|
|
64
|
+
--apply Perform migration: byte-preserving backup manifest + schema.json + journal
|
|
65
|
+
Without --apply, run is NOT executed (stays read-only)
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
--root <path> Memory root (default: ~/.dsh/memories)
|
|
69
|
+
--run-id <id> For verify: specific backup runId (default: latest / schema.json)
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
node scripts/migrate.mjs --root /tmp/memories # inspect only
|
|
73
|
+
node scripts/migrate.mjs --root /tmp/memories --dry-run # dry-run
|
|
74
|
+
node scripts/migrate.mjs --root /tmp/memories --apply # backup + adopt
|
|
75
|
+
node scripts/migrate.mjs --root /tmp/memories --verify # verify
|
|
76
|
+
|
|
77
|
+
`.trim()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function main(parsed: ParsedArgs): Promise<{ ok: boolean; command: string; output?: any }> {
|
|
81
|
+
if (parsed.help) {
|
|
82
|
+
console.log(helpText())
|
|
83
|
+
return { ok: true, command: 'help' }
|
|
84
|
+
}
|
|
85
|
+
const root = parsed.root ? resolveMemoryRoot(parsed.root) : resolveMemoryRoot(null)
|
|
86
|
+
|
|
87
|
+
if (parsed.command === 'verify') {
|
|
88
|
+
const res = await verify(root, parsed.runId)
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
console.log(`verify ok: runId=${res.runId} (${res.manifestPath})`)
|
|
91
|
+
} else {
|
|
92
|
+
console.error(`verify failed: runId=${res.runId}`)
|
|
93
|
+
for (const m of res.mismatches) console.error(` - ${m}`)
|
|
94
|
+
console.error('writes are now blocked until mismatch is resolved (see .maestro-memory/write-block.json)')
|
|
95
|
+
}
|
|
96
|
+
return { ok: res.ok, command: 'verify', output: res }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (parsed.command === 'run') {
|
|
100
|
+
if (!parsed.apply) {
|
|
101
|
+
// Safety: default is read-only, require explicit --apply
|
|
102
|
+
const insp = await inspect(root)
|
|
103
|
+
console.log('read-only inspect (dry-run): use --apply to perform migration')
|
|
104
|
+
console.log(`files: ${insp.files.filter((f) => f.exists).length} existing, warnings: ${insp.warnings.length}`)
|
|
105
|
+
for (const w of insp.warnings) console.log(` warn: ${w}`)
|
|
106
|
+
console.log(`inventory: memoryEntries=${insp.inventory.memoryEntries}, todos=${insp.inventory.todoIdsCount}, queue valid=${insp.inventory.queueValid} malformed=${insp.inventory.queueMalformed}`)
|
|
107
|
+
return { ok: true, command: 'inspect', output: insp }
|
|
108
|
+
}
|
|
109
|
+
const res = await run(root)
|
|
110
|
+
if (res.ok) {
|
|
111
|
+
console.log(`run ok: runId=${res.runId}`)
|
|
112
|
+
console.log(`manifest: ${res.manifestPath}`)
|
|
113
|
+
console.log(`backup: ${res.backupFilesDir}`)
|
|
114
|
+
if (res.warnings.length) {
|
|
115
|
+
console.log('warnings:')
|
|
116
|
+
for (const w of res.warnings) console.log(` - ${w}`)
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
console.error(`run failed: ${res.errors.join('; ')}`)
|
|
120
|
+
for (const w of res.warnings) console.error(` warn: ${w}`)
|
|
121
|
+
}
|
|
122
|
+
return { ok: res.ok, command: 'run', output: res }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (parsed.command === 'dryRun') {
|
|
126
|
+
const res = await dryRun(root)
|
|
127
|
+
console.log(`dryRun: ok=${res.ok}, warnings=${res.warnings.length}`)
|
|
128
|
+
for (const w of res.warnings) console.log(` warn: ${w}`)
|
|
129
|
+
console.log(`inventory: memoryEntries=${res.inventory.memoryEntries}, todos=${res.inventory.todoIdsCount}`)
|
|
130
|
+
return { ok: res.ok, command: 'dryRun', output: res }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// default inspect
|
|
134
|
+
const res = await inspect(root)
|
|
135
|
+
console.log(`inspect: ok=${res.ok}, files=${res.files.filter((f) => f.exists).length}, warnings=${res.warnings.length}`)
|
|
136
|
+
for (const w of res.warnings) console.log(` warn: ${w}`)
|
|
137
|
+
console.log(`inventory: memoryEntries=${res.inventory.memoryEntries}, todos=${res.inventory.todoIdsCount}, queue valid=${res.inventory.queueValid} malformed=${res.inventory.queueMalformed}`)
|
|
138
|
+
return { ok: res.ok, command: 'inspect', output: res }
|
|
139
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fixture.ts — helpers for M4-PR-A rehearsal: fixture profile with link: package/patch,
|
|
3
|
+
* one-owner per compatibility tool, and copied-schema population (not live home).
|
|
4
|
+
*
|
|
5
|
+
* Pure helpers + minimal FS. No Cordis import, no network, no live home mutation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
|
|
9
|
+
import { join, dirname } from 'node:path'
|
|
10
|
+
import { createHash } from 'node:crypto'
|
|
11
|
+
|
|
12
|
+
const COMPAT_TOOLS = ['memory', 'dtodo', 'skill_manage', 'memory_suggest', 'memory_review_status'] as const
|
|
13
|
+
type CompatTool = typeof COMPAT_TOOLS[number]
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Known tool ownership map for rehearsal.
|
|
17
|
+
* Currently only @ddtcorex/dsh-maestro-memory provides compatibility tools.
|
|
18
|
+
* Future: if DSH core adds an owner, this gate must surface it.
|
|
19
|
+
*/
|
|
20
|
+
const KNOWN_OWNERS: Record<string, string[]> = {
|
|
21
|
+
memory: ['@ddtcorex/dsh-maestro-memory'],
|
|
22
|
+
dtodo: ['@ddtcorex/dsh-maestro-memory'],
|
|
23
|
+
skill_manage: [], // optional module, not owned by default
|
|
24
|
+
memory_suggest: ['@ddtcorex/dsh-maestro-memory'],
|
|
25
|
+
memory_review_status: ['@ddtcorex/dsh-maestro-memory'],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CreateFixtureProfileOpts {
|
|
29
|
+
profileDir: string
|
|
30
|
+
packageDir: string
|
|
31
|
+
profileName?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FixtureProfileResult {
|
|
35
|
+
profileDir: string
|
|
36
|
+
packageJsonPath: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a fixture profile with link: package/patch and prove one owner per tool.
|
|
41
|
+
* - Creates <profileDir>/package.json with link: dependency to the local package checkout.
|
|
42
|
+
* - Bundles list contains exactly one owner for each compat tool.
|
|
43
|
+
* - Does NOT duplicate the cordis.patch.yml row (patch is owned by the package itself).
|
|
44
|
+
*/
|
|
45
|
+
export async function createFixtureProfile(opts: CreateFixtureProfileOpts): Promise<FixtureProfileResult> {
|
|
46
|
+
const { profileDir, packageDir } = opts
|
|
47
|
+
if (!profileDir) throw new Error('createFixtureProfile: profileDir is required')
|
|
48
|
+
if (!packageDir) throw new Error('createFixtureProfile: packageDir is required')
|
|
49
|
+
mkdirSync(profileDir, { recursive: true })
|
|
50
|
+
const packageJsonPath = join(profileDir, 'package.json')
|
|
51
|
+
const pkg = {
|
|
52
|
+
name: opts.profileName ?? 'dsh-profile-fixture',
|
|
53
|
+
private: true,
|
|
54
|
+
dsh: {
|
|
55
|
+
profile: {
|
|
56
|
+
bundles: ['@ddtcorex/dsh-maestro-memory'],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
dependencies: {
|
|
60
|
+
'@ddtcorex/dsh-maestro-memory': `link:${packageDir}`,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), 'utf8')
|
|
64
|
+
// Intentionally do NOT create cordis.patch.yml with duplicate maestro-memory row.
|
|
65
|
+
// The package's own cordis.patch.yml provides it; duplicates crash loader.
|
|
66
|
+
return { profileDir, packageJsonPath }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AssertSingleOwnerOpts {
|
|
70
|
+
toolOwners?: Record<string, string[]>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface AssertSingleOwnerResult {
|
|
74
|
+
ok: boolean
|
|
75
|
+
owners: Record<string, string>
|
|
76
|
+
errors: string[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Prove one owner per compatibility tool for a fixture profile.
|
|
81
|
+
* - By default derives owners from the profile's bundles (known mapping).
|
|
82
|
+
* - If opts.toolOwners provided, uses that mapping to simulate duplicate detection (for tests).
|
|
83
|
+
*/
|
|
84
|
+
export async function assertSingleOwner(
|
|
85
|
+
profileDir: string,
|
|
86
|
+
opts?: AssertSingleOwnerOpts,
|
|
87
|
+
): Promise<AssertSingleOwnerResult> {
|
|
88
|
+
const owners: Record<string, string> = {}
|
|
89
|
+
const errors: string[] = []
|
|
90
|
+
|
|
91
|
+
// If caller supplies explicit toolOwners mapping, use it for validation
|
|
92
|
+
if (opts?.toolOwners) {
|
|
93
|
+
for (const [tool, list] of Object.entries(opts.toolOwners)) {
|
|
94
|
+
if (list.length === 0) continue
|
|
95
|
+
if (list.length > 1) {
|
|
96
|
+
errors.push(`tool "${tool}" has multiple owners: ${list.join(', ')} (duplicate owner)`)
|
|
97
|
+
} else {
|
|
98
|
+
owners[tool] = list[0]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { ok: errors.length === 0, owners, errors }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Derive from profile package.json bundles
|
|
105
|
+
const pkgPath = join(profileDir, 'package.json')
|
|
106
|
+
if (!existsSync(pkgPath)) {
|
|
107
|
+
return { ok: false, owners, errors: [`profile package.json not found: ${pkgPath}`] }
|
|
108
|
+
}
|
|
109
|
+
let pkg: any
|
|
110
|
+
try {
|
|
111
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
112
|
+
} catch (e: any) {
|
|
113
|
+
return { ok: false, owners, errors: [`failed to parse package.json: ${e?.message}`] }
|
|
114
|
+
}
|
|
115
|
+
const bundles: string[] = pkg?.dsh?.profile?.bundles ?? []
|
|
116
|
+
const deps: Record<string, string> = pkg?.dependencies ?? {}
|
|
117
|
+
|
|
118
|
+
// For each compat tool, collect owners from bundles that claim it
|
|
119
|
+
for (const tool of COMPAT_TOOLS) {
|
|
120
|
+
const claimed = bundles.filter((b: string) => {
|
|
121
|
+
const known = KNOWN_OWNERS[tool] ?? []
|
|
122
|
+
return known.includes(b)
|
|
123
|
+
})
|
|
124
|
+
// Also check dependencies that are link: but not in bundles? Bundles is source of truth for loader.
|
|
125
|
+
if (claimed.length === 0) {
|
|
126
|
+
// tool not owned in this fixture — skip (skill_manage may be absent)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (claimed.length > 1) {
|
|
130
|
+
errors.push(`tool "${tool}" has multiple owners: ${claimed.join(', ')}`)
|
|
131
|
+
} else {
|
|
132
|
+
owners[tool] = claimed[0]
|
|
133
|
+
// Verify dependency is link:
|
|
134
|
+
const depVal = deps[claimed[0]]
|
|
135
|
+
if (depVal && !depVal.startsWith('link:')) {
|
|
136
|
+
errors.push(`tool "${tool}" owner ${claimed[0]} dependency is not link: (got ${depVal})`)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Additional check: ensure profile does not contain duplicate patch row
|
|
142
|
+
const patchPath = join(profileDir, 'cordis.patch.yml')
|
|
143
|
+
if (existsSync(patchPath)) {
|
|
144
|
+
const text = readFileSync(patchPath, 'utf8')
|
|
145
|
+
if (text.includes('maestro-memory')) {
|
|
146
|
+
errors.push('profile cordis.patch.yml must not duplicate package patch id maestro-memory')
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check that our package link exists
|
|
151
|
+
const ourDep = deps['@ddtcorex/dsh-maestro-memory']
|
|
152
|
+
if (!ourDep) {
|
|
153
|
+
errors.push('profile missing dependency @ddtcorex/dsh-maestro-memory')
|
|
154
|
+
} else if (!ourDep.startsWith('link:')) {
|
|
155
|
+
errors.push(`expected link: dependency for @ddtcorex/dsh-maestro-memory, got ${ourDep}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { ok: errors.length === 0, owners, errors }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Copied schema fixture (not live home)
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
function writeLegacyMemory(file: string, entries: string[]) {
|
|
166
|
+
const content = entries.length === 0 ? '' : entries.join('\n§\n') + '\n'
|
|
167
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
168
|
+
writeFileSync(file, content, 'utf8')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function writeTodoFile(file: string, entriesRaw: string[]) {
|
|
172
|
+
const header = `<!--\nTodo entry format (auto-maintained by the program, do not edit the structure manually):\n- Entries are delimited by §; the comment block before the first § is the format note, not a todo\n- The first line of each todo is the metadata tag line (fixed order, optional parts may be omitted):\n [created time] auto-stamped by the program (e.g. [2026-08-06 21:30])\n [id: 8-hex] unique identifier for the entry, operated by the dtodo tool\n [q1] important & urgent [q2] important not urgent [q3] urgent not important [q4] not important not urgent (default = unclassified)\n [due: YYYY-MM-DD] due date (default = none)\n [status: pending|doing|done|blocked|cancelled] status (default pending)\n [done: YYYY-MM-DD HH:MM] completion time (auto-stamped, only for done status)\n [cat: category] optional (life/work/study...)\n- Todo content follows the first tag line and may span multiple lines\n-->\n`
|
|
173
|
+
const body = entriesRaw.join('\n§\n')
|
|
174
|
+
const text = `${header}${body.length > 0 ? `\n§\n${body}\n` : ''}`
|
|
175
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
176
|
+
writeFileSync(file, text, 'utf8')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Populate a temp memory root with synthetic legacy files for rehearsal,
|
|
181
|
+
* mirroring migration.spec's fixture but centralized for reuse.
|
|
182
|
+
* This root is a *copy*, never the live ~/.dsh/memories.
|
|
183
|
+
*/
|
|
184
|
+
export async function createCopiedMemoryRoot(root: string, opts?: { cwd?: string }) {
|
|
185
|
+
const cwd = opts?.cwd ?? '/tmp/proj-a'
|
|
186
|
+
const hash = createHash('sha1').update(cwd).digest('hex').slice(0, 12)
|
|
187
|
+
|
|
188
|
+
// global memory
|
|
189
|
+
writeLegacyMemory(join(root, 'MEMORY.md'), ['[2026-08-10] global one', '[2026-08-11] global two'])
|
|
190
|
+
writeLegacyMemory(join(root, 'USER.md'), ['[2026-08-10] user one'])
|
|
191
|
+
writeLegacyMemory(join(root, 'MEMORY-archive.md'), ['[2026-08-09] archived global'])
|
|
192
|
+
writeLegacyMemory(join(root, 'USER-archive.md'), ['[2026-08-09] archived user'])
|
|
193
|
+
// queue
|
|
194
|
+
const suggestion = { target: 'memory', content: 'suggest 1', reason: 'r', time: new Date().toISOString() }
|
|
195
|
+
mkdirSync(root, { recursive: true })
|
|
196
|
+
writeFileSync(join(root, 'SUGGESTIONS.jsonl'), JSON.stringify(suggestion) + '\n', 'utf8')
|
|
197
|
+
// project
|
|
198
|
+
writeLegacyMemory(join(root, 'projects', hash, 'MEMORY.md'), ['[2026-08-10 10:00] project log'])
|
|
199
|
+
writeLegacyMemory(join(root, 'projects', hash, 'KEY.md'), ['[2026-08-10] [branch:main] key entry', '[2026-08-10] key two'])
|
|
200
|
+
writeLegacyMemory(join(root, 'projects', hash, 'KEY-archive.md'), ['[2026-08-08] old key'])
|
|
201
|
+
writeFileSync(
|
|
202
|
+
join(root, 'projects', hash, 'TODOS.md'),
|
|
203
|
+
'<!-- header -->\n§\n[2026-08-10 10:00] [id: aabbccdd] [status: pending]\nproj todo\n',
|
|
204
|
+
'utf8',
|
|
205
|
+
)
|
|
206
|
+
// daily
|
|
207
|
+
mkdirSync(join(root, 'daily'), { recursive: true })
|
|
208
|
+
writeFileSync(join(root, 'daily', '2026-08-10.md'), '[08:30] daily entry\n', 'utf8')
|
|
209
|
+
writeFileSync(
|
|
210
|
+
join(root, 'daily', '2026-08-10.todo.md'),
|
|
211
|
+
'<!-- header -->\n§\n[2026-08-10 10:00] [id: deadbeef] [status: pending]\ndaily todo\n',
|
|
212
|
+
'utf8',
|
|
213
|
+
)
|
|
214
|
+
// todos life/work
|
|
215
|
+
writeTodoFile(join(root, 'TODOS-life.md'), ['[2026-08-10 10:00] [id: 11111111] [status: pending]\nlife todo'])
|
|
216
|
+
writeTodoFile(join(root, 'TODOS-work.md'), ['[2026-08-10 10:00] [id: 22222222] [status: doing]\nwork todo'])
|
|
217
|
+
writeFileSync(join(root, 'TODO-archive.md'), 'archived todo\n', 'utf8')
|
|
218
|
+
}
|