devworkspace 0.0.0-stage → 0.1.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 +24 -0
- package/LICENSE +21 -0
- package/README.md +127 -3
- package/dist/agents/claude/adapter.d.ts +10 -0
- package/dist/agents/claude/adapter.js +97 -0
- package/dist/agents/claude/adapter.js.map +1 -0
- package/dist/agents/claude/content.d.ts +7 -0
- package/dist/agents/claude/content.js +136 -0
- package/dist/agents/claude/content.js.map +1 -0
- package/dist/agents/types.d.ts +26 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/cli/args.d.ts +194 -0
- package/dist/cli/args.js +93 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/help.d.ts +10 -0
- package/dist/cli/help.js +169 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +19 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/render.d.ts +39 -0
- package/dist/cli/render.js +211 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/run.d.ts +10 -0
- package/dist/cli/run.js +329 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/config/schemas.d.ts +76 -0
- package/dist/config/schemas.js +83 -0
- package/dist/config/schemas.js.map +1 -0
- package/dist/config/validate.d.ts +22 -0
- package/dist/config/validate.js +76 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/core/migrations.d.ts +15 -0
- package/dist/core/migrations.js +34 -0
- package/dist/core/migrations.js.map +1 -0
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.js +12 -0
- package/dist/core/version.js.map +1 -0
- package/dist/core/versioned.d.ts +13 -0
- package/dist/core/versioned.js +16 -0
- package/dist/core/versioned.js.map +1 -0
- package/dist/core/workspace.d.ts +34 -0
- package/dist/core/workspace.js +92 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/decisions/decision.d.ts +31 -0
- package/dist/decisions/decision.js +123 -0
- package/dist/decisions/decision.js.map +1 -0
- package/dist/discovery/commands.d.ts +12 -0
- package/dist/discovery/commands.js +92 -0
- package/dist/discovery/commands.js.map +1 -0
- package/dist/discovery/detect.d.ts +72 -0
- package/dist/discovery/detect.js +421 -0
- package/dist/discovery/detect.js.map +1 -0
- package/dist/discovery/ignore.d.ts +6 -0
- package/dist/discovery/ignore.js +76 -0
- package/dist/discovery/ignore.js.map +1 -0
- package/dist/discovery/scan.d.ts +24 -0
- package/dist/discovery/scan.js +132 -0
- package/dist/discovery/scan.js.map +1 -0
- package/dist/errors/index.d.ts +25 -0
- package/dist/errors/index.js +39 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/git/git.d.ts +27 -0
- package/dist/git/git.js +97 -0
- package/dist/git/git.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/agent-skills.d.ts +10 -0
- package/dist/integrations/agent-skills.js +45 -0
- package/dist/integrations/agent-skills.js.map +1 -0
- package/dist/integrations/graphify.d.ts +7 -0
- package/dist/integrations/graphify.js +54 -0
- package/dist/integrations/graphify.js.map +1 -0
- package/dist/integrations/index.d.ts +6 -0
- package/dist/integrations/index.js +30 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/omniroute.d.ts +7 -0
- package/dist/integrations/omniroute.js +24 -0
- package/dist/integrations/omniroute.js.map +1 -0
- package/dist/integrations/ponytail.d.ts +10 -0
- package/dist/integrations/ponytail.js +58 -0
- package/dist/integrations/ponytail.js.map +1 -0
- package/dist/integrations/probe.d.ts +6 -0
- package/dist/integrations/probe.js +41 -0
- package/dist/integrations/probe.js.map +1 -0
- package/dist/integrations/types.d.ts +25 -0
- package/dist/integrations/types.js +2 -0
- package/dist/integrations/types.js.map +1 -0
- package/dist/knowledge/facts.d.ts +43 -0
- package/dist/knowledge/facts.js +195 -0
- package/dist/knowledge/facts.js.map +1 -0
- package/dist/portability/bundle.d.ts +22 -0
- package/dist/portability/bundle.js +102 -0
- package/dist/portability/bundle.js.map +1 -0
- package/dist/prompts/onboarding.d.ts +19 -0
- package/dist/prompts/onboarding.js +53 -0
- package/dist/prompts/onboarding.js.map +1 -0
- package/dist/prompts/refresh.d.ts +15 -0
- package/dist/prompts/refresh.js +33 -0
- package/dist/prompts/refresh.js.map +1 -0
- package/dist/prompts/requirements.d.ts +9 -0
- package/dist/prompts/requirements.js +34 -0
- package/dist/prompts/requirements.js.map +1 -0
- package/dist/security/files.d.ts +5 -0
- package/dist/security/files.js +44 -0
- package/dist/security/files.js.map +1 -0
- package/dist/security/redact.d.ts +13 -0
- package/dist/security/redact.js +43 -0
- package/dist/security/redact.js.map +1 -0
- package/dist/services/agent.d.ts +11 -0
- package/dist/services/agent.js +42 -0
- package/dist/services/agent.js.map +1 -0
- package/dist/services/analysis.d.ts +45 -0
- package/dist/services/analysis.js +151 -0
- package/dist/services/analysis.js.map +1 -0
- package/dist/services/common.d.ts +18 -0
- package/dist/services/common.js +18 -0
- package/dist/services/common.js.map +1 -0
- package/dist/services/doctor.d.ts +27 -0
- package/dist/services/doctor.js +223 -0
- package/dist/services/doctor.js.map +1 -0
- package/dist/services/init.d.ts +18 -0
- package/dist/services/init.js +135 -0
- package/dist/services/init.js.map +1 -0
- package/dist/services/knowledge.d.ts +40 -0
- package/dist/services/knowledge.js +72 -0
- package/dist/services/knowledge.js.map +1 -0
- package/dist/services/migrate.d.ts +16 -0
- package/dist/services/migrate.js +54 -0
- package/dist/services/migrate.js.map +1 -0
- package/dist/services/portability.d.ts +27 -0
- package/dist/services/portability.js +155 -0
- package/dist/services/portability.js.map +1 -0
- package/dist/services/records.d.ts +8 -0
- package/dist/services/records.js +26 -0
- package/dist/services/records.js.map +1 -0
- package/dist/services/refresh.d.ts +22 -0
- package/dist/services/refresh.js +64 -0
- package/dist/services/refresh.js.map +1 -0
- package/dist/services/status.d.ts +57 -0
- package/dist/services/status.js +111 -0
- package/dist/services/status.js.map +1 -0
- package/dist/services/tasks.d.ts +44 -0
- package/dist/services/tasks.js +136 -0
- package/dist/services/tasks.js.map +1 -0
- package/dist/sessions/session.d.ts +19 -0
- package/dist/sessions/session.js +53 -0
- package/dist/sessions/session.js.map +1 -0
- package/dist/tasks/routing.d.ts +27 -0
- package/dist/tasks/routing.js +206 -0
- package/dist/tasks/routing.js.map +1 -0
- package/dist/tasks/task.d.ts +90 -0
- package/dist/tasks/task.js +275 -0
- package/dist/tasks/task.js.map +1 -0
- package/dist/utils/cleanup.d.ts +3 -0
- package/dist/utils/cleanup.js +18 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/fs.d.ts +17 -0
- package/dist/utils/fs.js +164 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/lock.d.ts +6 -0
- package/dist/utils/lock.js +77 -0
- package/dist/utils/lock.js.map +1 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +18 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.d.ts +18 -0
- package/dist/utils/paths.js +95 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/proc.d.ts +22 -0
- package/dist/utils/proc.js +74 -0
- package/dist/utils/proc.js.map +1 -0
- package/docs/architecture.md +53 -0
- package/docs/benchmarking.md +33 -0
- package/docs/claude.md +46 -0
- package/docs/contributing.md +64 -0
- package/docs/getting-started.md +68 -0
- package/docs/integrations.md +51 -0
- package/docs/knowledge.md +66 -0
- package/docs/portability.md +40 -0
- package/docs/routing.md +51 -0
- package/docs/security.md +44 -0
- package/docs/testing.md +58 -0
- package/docs/troubleshooting.md +26 -0
- package/package.json +68 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task.js","sourceRoot":"","sources":["../../src/tasks/task.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport { Check } from '../config/validate.js';\nimport { SCHEMA_VERSION, type CommandEntry } from '../config/schemas.js';\nimport { atomicWriteJson, ensureDir, pathExists, randomId } from '../utils/fs.js';\nimport { readVersioned } from '../core/versioned.js';\nimport { backupIfVersionChanges, type Ws } from '../core/workspace.js';\nimport { containsSecret } from '../security/redact.js';\n\nexport const TASK_TYPES = [\n 'QUESTION',\n 'BUG',\n 'FEATURE',\n 'REFACTOR',\n 'DOCUMENTATION',\n 'TESTING',\n 'PERFORMANCE',\n 'SECURITY',\n 'ARCHITECTURE',\n 'RELEASE',\n 'MAINTENANCE',\n] as const;\nexport type TaskType = (typeof TASK_TYPES)[number];\n\nexport const TASK_STATUSES = ['IDEA', 'SPECIFIED', 'PLANNED', 'IN_PROGRESS', 'TESTING', 'REVIEW', 'BLOCKED', 'FAILED', 'COMPLETED'] as const;\nexport type TaskStatus = (typeof TASK_STATUSES)[number];\n\nexport const VERIFY_STATUSES = ['PENDING', 'PASS', 'FAIL', 'SKIPPED'] as const;\nexport type VerifyStatus = (typeof VERIFY_STATUSES)[number];\n\nexport interface VerificationItem {\n check: string;\n required: boolean;\n status: VerifyStatus;\n reason?: string;\n detail?: string;\n updatedAt?: string;\n}\n\nexport interface TestCounts {\n passed: number;\n failed: number;\n}\n\nexport interface Attestations {\n userWorkIntact: boolean;\n knowledgeReviewed: boolean;\n finalStateInspected: boolean;\n}\n\nexport interface Task {\n schemaVersion: number;\n id: string;\n title: string;\n type: TaskType;\n status: TaskStatus;\n createdAt: string;\n updatedAt: string;\n description: string;\n acceptanceCriteria: string[];\n plan: string[];\n affectedAreas: string[];\n verification: VerificationItem[];\n tests: { baseline?: TestCounts; after?: TestCounts };\n attestations: Attestations;\n decisions: string[];\n blockers: string[];\n constraints: string[];\n completionSummary?: string;\n history: { at: string; from: TaskStatus | null; to: TaskStatus; note?: string }[];\n}\n\nconst TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {\n IDEA: ['SPECIFIED', 'PLANNED', 'IN_PROGRESS', 'BLOCKED', 'FAILED'],\n SPECIFIED: ['PLANNED', 'IN_PROGRESS', 'BLOCKED', 'IDEA'],\n PLANNED: ['IN_PROGRESS', 'SPECIFIED', 'BLOCKED', 'IDEA'],\n IN_PROGRESS: ['TESTING', 'REVIEW', 'COMPLETED', 'BLOCKED', 'FAILED', 'PLANNED'],\n TESTING: ['IN_PROGRESS', 'REVIEW', 'COMPLETED', 'BLOCKED', 'FAILED'],\n REVIEW: ['IN_PROGRESS', 'TESTING', 'COMPLETED', 'BLOCKED', 'FAILED'],\n BLOCKED: ['IN_PROGRESS', 'PLANNED', 'SPECIFIED', 'IDEA', 'FAILED'],\n FAILED: ['IN_PROGRESS', 'IDEA'],\n COMPLETED: ['IN_PROGRESS'],\n};\n\nexport function allowedTransitions(from: TaskStatus): TaskStatus[] {\n return TRANSITIONS[from];\n}\n\nconst CODE_CHANGING: TaskType[] = ['BUG', 'FEATURE', 'REFACTOR', 'PERFORMANCE', 'SECURITY', 'ARCHITECTURE', 'MAINTENANCE', 'TESTING', 'RELEASE'];\n\nconst CHECKS_BY_TYPE: Record<TaskType, string[]> = {\n QUESTION: [],\n DOCUMENTATION: [],\n BUG: ['unit', 'typecheck', 'lint', 'build'],\n FEATURE: ['unit', 'typecheck', 'lint', 'build', 'review'],\n REFACTOR: ['unit', 'typecheck', 'lint', 'build', 'review'],\n TESTING: ['unit', 'typecheck', 'lint'],\n PERFORMANCE: ['unit', 'build', 'review'],\n SECURITY: ['unit', 'typecheck', 'lint', 'build', 'review'],\n ARCHITECTURE: ['unit', 'typecheck', 'lint', 'build', 'review'],\n RELEASE: ['unit', 'typecheck', 'lint', 'build', 'integration', 'e2e'],\n MAINTENANCE: ['lint', 'build'],\n};\n\nexport function isCodeChanging(type: TaskType): boolean {\n return CODE_CHANGING.includes(type);\n}\n\n/** Verification a task of this type needs, given what the project actually offers. Unavailable checks are never demanded. */\nexport function requiredChecksFor(type: TaskType, contract: CommandEntry[], reviewLevel: 'off' | 'light' | 'strict' = 'light'): VerificationItem[] {\n const available = new Set(contract.filter((c) => c.available).map((c) => c.id as string));\n const items: VerificationItem[] = [];\n for (const check of CHECKS_BY_TYPE[type]) {\n if (check === 'review') {\n if (reviewLevel === 'off') continue;\n } else if (!available.has(check)) continue;\n items.push({ check, required: true, status: 'PENDING' });\n }\n return items;\n}\n\nexport function newTaskId(now: Date = new Date()): string {\n const d = now.toISOString().slice(0, 10).replace(/-/g, '');\n return `T-${d}-${randomId(2)}`;\n}\n\nexport interface NewTaskInput {\n title: string;\n type: TaskType;\n description?: string;\n verification: VerificationItem[];\n constraints?: string[];\n}\n\nexport function createTaskObject(input: NewTaskInput, now: string, id = newTaskId()): Task {\n const title = input.title.trim().slice(0, 200);\n if (!title) throw new DevWorkspaceError('USAGE', 'Task title must not be empty.');\n const task: Task = {\n schemaVersion: SCHEMA_VERSION,\n id,\n title,\n type: input.type,\n status: 'IDEA',\n createdAt: now,\n updatedAt: now,\n description: (input.description ?? '').trim().slice(0, 4000),\n acceptanceCriteria: [],\n plan: [],\n affectedAreas: [],\n verification: input.verification,\n tests: {},\n attestations: { userWorkIntact: false, knowledgeReviewed: false, finalStateInspected: false },\n decisions: [],\n blockers: [],\n constraints: input.constraints ?? [],\n history: [{ at: now, from: null, to: 'IDEA' }],\n };\n assertNoSecrets(task);\n return task;\n}\n\nexport function assertNoSecrets(task: Task): void {\n if (containsSecret(JSON.stringify(task))) {\n throw new DevWorkspaceError('SECURITY', 'Task content looks like it contains a secret; refusing to store it.', {\n hint: 'Remove credential values from titles, plans and notes.',\n });\n }\n}\n\nexport function validateTask(v: unknown, file = 'task'): Task {\n const c = new Check(file);\n const o = c.obj(v);\n c.at_('schemaVersion').int(o.schemaVersion, 1);\n c.at_('id').str(o.id, { min: 3, max: 64 });\n c.at_('title').str(o.title, { min: 1, max: 300 });\n c.at_('type').oneOf(o.type, TASK_TYPES);\n c.at_('status').oneOf(o.status, TASK_STATUSES);\n c.at_('createdAt').iso(o.createdAt);\n c.at_('updatedAt').iso(o.updatedAt);\n for (const k of ['acceptanceCriteria', 'plan', 'affectedAreas', 'decisions', 'blockers', 'constraints'] as const) c.at_(k).strArr(o[k]);\n c.at_('verification')\n .arr(o.verification, 100)\n .forEach((item, i) => {\n const ic = c.at_(`verification.${i}`);\n const io = ic.obj(item);\n ic.at_('check').str(io.check, { max: 50 });\n ic.at_('required').bool(io.required);\n ic.at_('status').oneOf(io.status, VERIFY_STATUSES);\n });\n const a = c.at_('attestations').obj(o.attestations);\n c.at_('attestations.userWorkIntact').bool(a.userWorkIntact);\n c.at_('attestations.knowledgeReviewed').bool(a.knowledgeReviewed);\n c.at_('attestations.finalStateInspected').bool(a.finalStateInspected);\n c.at_('tests').obj(o.tests);\n c.at_('history').arr(o.history, 500);\n return o as unknown as Task;\n}\n\nexport interface GateResult {\n ok: boolean;\n failures: string[];\n warnings: string[];\n}\n\n/** The completion gate. A task is only COMPLETED when every applicable requirement holds. */\nexport function evaluateGate(task: Task): GateResult {\n const failures: string[] = [];\n const warnings: string[] = [];\n for (const v of task.verification) {\n if (!v.required) continue;\n if (v.status === 'FAIL') failures.push(`${v.check}: FAIL`);\n else if (v.status === 'PENDING') failures.push(`${v.check}: not run yet`);\n else if (v.status === 'SKIPPED') {\n if (v.reason === 'user-override') warnings.push(`${v.check}: skipped at the user's request; completion is less certain`);\n else failures.push(`${v.check}: skipped without a user override`);\n }\n }\n const { baseline, after } = task.tests;\n if (baseline && !after) failures.push('baseline test results recorded, but no results after the change');\n if (baseline && after) {\n if (after.failed > baseline.failed) failures.push(`regression: failures rose from ${baseline.failed} to ${after.failed}`);\n if (after.passed < baseline.passed) failures.push(`regression: passing tests fell from ${baseline.passed} to ${after.passed}`);\n }\n if (isCodeChanging(task.type)) {\n if (!task.attestations.userWorkIntact) failures.push('not confirmed: no user work was overwritten');\n if (!task.attestations.knowledgeReviewed) failures.push('not confirmed: project knowledge was reviewed for updates');\n if (!task.attestations.finalStateInspected) failures.push('not confirmed: final repository state was inspected');\n }\n if ((isCodeChanging(task.type) || task.type === 'DOCUMENTATION') && !(task.completionSummary ?? '').trim()) {\n failures.push('completion summary is missing');\n }\n return { ok: failures.length === 0, failures, warnings };\n}\n\nexport function transitionTask(task: Task, to: TaskStatus, now: string, note?: string): GateResult | null {\n if (task.status === to) return null;\n if (!TRANSITIONS[task.status].includes(to)) {\n throw new DevWorkspaceError('GATE', `Cannot move a task from ${task.status} to ${to}.`, {\n hint: `Allowed from ${task.status}: ${TRANSITIONS[task.status].join(', ')}.`,\n });\n }\n let gate: GateResult | null = null;\n if (to === 'COMPLETED') {\n gate = evaluateGate(task);\n if (!gate.ok) {\n throw new DevWorkspaceError('GATE', `Task ${task.id} cannot be completed:\\n - ${gate.failures.join('\\n - ')}`, {\n hint: 'Resolve the items above (or mark the task BLOCKED/FAILED). A task is never complete while verification is failing.',\n });\n }\n }\n task.history.push({ at: now, from: task.status, to, ...(note ? { note: note.slice(0, 300) } : {}) });\n task.status = to;\n task.updatedAt = now;\n return gate;\n}\n\nexport function nextRequiredStep(task: Task): string {\n switch (task.status) {\n case 'COMPLETED':\n return 'None. The task is complete.';\n case 'FAILED':\n return 'Review the failure, then move the task back to IN_PROGRESS or close the idea.';\n case 'BLOCKED':\n return task.blockers.length > 0 ? `Resolve blocker: ${task.blockers[task.blockers.length - 1]}` : 'Identify and record what blocks this task.';\n case 'IDEA':\n return 'Clarify acceptance criteria, then plan.';\n case 'SPECIFIED':\n return 'Write a plan.';\n case 'PLANNED':\n return 'Start implementation (baseline verification first for meaningful changes).';\n default: {\n const pending = task.verification.filter((v) => v.required && (v.status === 'PENDING' || v.status === 'FAIL'));\n if (pending.length > 0) return `Run/fix verification: ${pending.map((p) => `${p.check} (${p.status.toLowerCase()})`).join(', ')}.`;\n const g = evaluateGate(task);\n return g.ok ? 'All gates pass. Mark the task COMPLETED.' : `Satisfy the completion gate: ${g.failures[0]}`;\n }\n }\n}\n\n// ---------- storage ----------\n\nfunction taskFile(ws: Ws, task: Pick<Task, 'id' | 'status'>): string {\n return path.join(task.status === 'COMPLETED' ? ws.tasksCompleted : ws.tasksActive, `${task.id}.json`);\n}\n\nexport async function saveTask(ws: Ws, task: Task): Promise<void> {\n validateTask(task);\n assertNoSecrets(task);\n const dest = taskFile(ws, task);\n await backupIfVersionChanges(ws, dest, task.schemaVersion);\n await ensureDir(path.dirname(dest));\n await atomicWriteJson(dest, task);\n const other = path.join(task.status === 'COMPLETED' ? ws.tasksActive : ws.tasksCompleted, `${task.id}.json`);\n await fs.rm(other, { force: true });\n}\n\nexport async function loadTask(ws: Ws, idOrPrefix: string): Promise<Task> {\n for (const dir of [ws.tasksActive, ws.tasksCompleted]) {\n if (!(await pathExists(dir))) continue;\n const names = (await fs.readdir(dir)).filter((n) => n.endsWith('.json'));\n const hits = names.filter((n) => n === `${idOrPrefix}.json` || n.startsWith(idOrPrefix));\n if (hits.length > 1) {\n const exact = hits.find((n) => n === `${idOrPrefix}.json`);\n if (!exact) throw new DevWorkspaceError('USAGE', `Task id \"${idOrPrefix}\" is ambiguous.`, { hint: 'Use more characters of the id.' });\n hits.splice(0, hits.length, exact);\n }\n if (hits.length === 1) {\n const file = path.join(dir, hits[0]!);\n return (await readVersioned(file, 'task', SCHEMA_VERSION, validateTask)).data;\n }\n }\n throw new DevWorkspaceError('NOT_FOUND', `No task matching \"${idOrPrefix}\".`, { hint: 'List tasks with \"devworkspace task list --all\".' });\n}\n\nexport interface TaskListing {\n tasks: Task[];\n corrupt: { file: string; error: string }[];\n}\n\nexport async function listTasks(ws: Ws, opts: { includeCompleted?: boolean } = {}): Promise<TaskListing> {\n const tasks: Task[] = [];\n const corrupt: { file: string; error: string }[] = [];\n const dirs = opts.includeCompleted ? [ws.tasksActive, ws.tasksCompleted] : [ws.tasksActive];\n for (const dir of dirs) {\n if (!(await pathExists(dir))) continue;\n for (const name of (await fs.readdir(dir)).filter((n) => n.endsWith('.json')).sort()) {\n try {\n tasks.push((await readVersioned(path.join(dir, name), 'task', SCHEMA_VERSION, validateTask)).data);\n } catch (e) {\n corrupt.push({ file: `${path.basename(dir)}/${name}`, error: (e as Error).message });\n }\n }\n }\n tasks.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));\n return { tasks, corrupt };\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const callbacks = new Set();
|
|
2
|
+
/** Register a synchronous cleanup (e.g. release a lock). Returns a disposer. */
|
|
3
|
+
export function onCleanup(fn) {
|
|
4
|
+
callbacks.add(fn);
|
|
5
|
+
return () => callbacks.delete(fn);
|
|
6
|
+
}
|
|
7
|
+
export function runCleanups() {
|
|
8
|
+
for (const fn of [...callbacks]) {
|
|
9
|
+
try {
|
|
10
|
+
fn();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
/* best effort */
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
callbacks.clear();
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/utils/cleanup.ts"],"names":[],"mappings":"AAAA,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;AAExC,gFAAgF;AAChF,MAAM,UAAU,SAAS,CAAC,EAAc;IACtC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC;QAChC,IAAI,CAAC;YACH,EAAE,EAAE,CAAC;QACP,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB;QACnB,CAAC;IACH,CAAC;IACD,SAAS,CAAC,KAAK,EAAE,CAAC;AACpB,CAAC","sourcesContent":["const callbacks = new Set<() => void>();\n\n/** Register a synchronous cleanup (e.g. release a lock). Returns a disposer. */\nexport function onCleanup(fn: () => void): () => void {\n callbacks.add(fn);\n return () => callbacks.delete(fn);\n}\n\nexport function runCleanups(): void {\n for (const fn of [...callbacks]) {\n try {\n fn();\n } catch {\n /* best effort */\n }\n }\n callbacks.clear();\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const MAX_JSON_BYTES: number;
|
|
2
|
+
export declare function pathExists(p: string): Promise<boolean>;
|
|
3
|
+
export declare function ensureDir(p: string): Promise<void>;
|
|
4
|
+
/** Write to a temp file in the same directory, flush it, then atomically rename into place. */
|
|
5
|
+
export declare function atomicWriteFile(file: string, data: string | Uint8Array, validate?: (data: string) => void): Promise<void>;
|
|
6
|
+
export declare function atomicWriteJson(file: string, value: unknown): Promise<void>;
|
|
7
|
+
export declare function readJsonFile(file: string, maxBytes?: number): Promise<unknown>;
|
|
8
|
+
export declare function sha256File(file: string): Promise<string>;
|
|
9
|
+
export declare function sha256Text(text: string): string;
|
|
10
|
+
export declare function looksBinary(buf: Uint8Array): boolean;
|
|
11
|
+
/** Reads a text file; returns null if binary or larger than maxBytes. */
|
|
12
|
+
export declare function readTextBounded(file: string, maxBytes?: number): Promise<string | null>;
|
|
13
|
+
export declare function randomId(bytes?: number): string;
|
|
14
|
+
export declare function listFilesRecursive(dir: string, maxFiles?: number): Promise<string[]>;
|
|
15
|
+
/** Atomically write text only when it differs from what is on disk. Returns true if a write happened. */
|
|
16
|
+
export declare function writeTextIfChanged(file: string, text: string): Promise<boolean>;
|
|
17
|
+
export declare function writeJsonIfChanged(file: string, value: unknown): Promise<boolean>;
|
package/dist/utils/fs.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { promises as fs, createReadStream } from 'node:fs';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DevWorkspaceError } from '../errors/index.js';
|
|
5
|
+
export const MAX_JSON_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
export async function pathExists(p) {
|
|
7
|
+
try {
|
|
8
|
+
await fs.access(p);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function ensureDir(p) {
|
|
16
|
+
await fs.mkdir(p, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
async function renameWithRetry(from, to) {
|
|
19
|
+
let lastErr;
|
|
20
|
+
// Windows briefly denies renames while another process (reader, indexer, editor file watcher) holds the target.
|
|
21
|
+
for (let i = 0; i < 20; i++) {
|
|
22
|
+
try {
|
|
23
|
+
await fs.rename(from, to);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
lastErr = e;
|
|
28
|
+
const code = e.code;
|
|
29
|
+
if (code !== 'EPERM' && code !== 'EBUSY' && code !== 'EACCES')
|
|
30
|
+
break;
|
|
31
|
+
await new Promise((r) => setTimeout(r, Math.min(25 * (i + 1), 150)));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw lastErr;
|
|
35
|
+
}
|
|
36
|
+
/** Write to a temp file in the same directory, flush it, then atomically rename into place. */
|
|
37
|
+
export async function atomicWriteFile(file, data, validate) {
|
|
38
|
+
await ensureDir(path.dirname(file));
|
|
39
|
+
if (validate && typeof data === 'string')
|
|
40
|
+
validate(data);
|
|
41
|
+
const tmp = `${file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
42
|
+
let fh;
|
|
43
|
+
try {
|
|
44
|
+
fh = await fs.open(tmp, 'wx');
|
|
45
|
+
await fh.writeFile(data);
|
|
46
|
+
await fh.sync();
|
|
47
|
+
await fh.close();
|
|
48
|
+
fh = undefined;
|
|
49
|
+
await renameWithRetry(tmp, file);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (fh)
|
|
53
|
+
await fh.close().catch(() => undefined);
|
|
54
|
+
await fs.rm(tmp, { force: true }).catch(() => undefined);
|
|
55
|
+
throw new DevWorkspaceError('IO', `Could not write ${path.basename(file)}.`, {
|
|
56
|
+
cause: e,
|
|
57
|
+
hint: 'Check file permissions and free disk space.',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function atomicWriteJson(file, value) {
|
|
62
|
+
const text = JSON.stringify(value, null, 2) + '\n';
|
|
63
|
+
await atomicWriteFile(file, text, (t) => {
|
|
64
|
+
JSON.parse(t);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export async function readJsonFile(file, maxBytes = MAX_JSON_BYTES) {
|
|
68
|
+
let stat;
|
|
69
|
+
try {
|
|
70
|
+
stat = await fs.stat(file);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
throw new DevWorkspaceError('NOT_FOUND', `File not found: ${path.basename(file)}`, { cause: e });
|
|
74
|
+
}
|
|
75
|
+
if (stat.size > maxBytes) {
|
|
76
|
+
throw new DevWorkspaceError('SCHEMA', `${path.basename(file)} is unreasonably large (${stat.size} bytes).`, {
|
|
77
|
+
hint: 'Restore it from version control or a backup in .devworkspace/local/backups.',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const text = await fs.readFile(file, 'utf8');
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(text.replace(/^\uFEFF/, ''));
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
throw new DevWorkspaceError('SCHEMA', `${path.basename(file)} contains malformed JSON.`, {
|
|
86
|
+
cause: e,
|
|
87
|
+
hint: 'Restore the file from version control (git checkout -- <file>) or from .devworkspace/local/backups.',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function sha256File(file) {
|
|
92
|
+
const hash = createHash('sha256');
|
|
93
|
+
await new Promise((resolve, reject) => {
|
|
94
|
+
const s = createReadStream(file);
|
|
95
|
+
s.on('data', (c) => hash.update(c));
|
|
96
|
+
s.on('error', reject);
|
|
97
|
+
s.on('end', () => resolve());
|
|
98
|
+
});
|
|
99
|
+
return hash.digest('hex');
|
|
100
|
+
}
|
|
101
|
+
export function sha256Text(text) {
|
|
102
|
+
return createHash('sha256').update(text).digest('hex');
|
|
103
|
+
}
|
|
104
|
+
export function looksBinary(buf) {
|
|
105
|
+
const n = Math.min(buf.length, 8000);
|
|
106
|
+
for (let i = 0; i < n; i++)
|
|
107
|
+
if (buf[i] === 0)
|
|
108
|
+
return true;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
/** Reads a text file; returns null if binary or larger than maxBytes. */
|
|
112
|
+
export async function readTextBounded(file, maxBytes = 512 * 1024) {
|
|
113
|
+
const stat = await fs.stat(file);
|
|
114
|
+
if (stat.size > maxBytes)
|
|
115
|
+
return null;
|
|
116
|
+
const buf = await fs.readFile(file);
|
|
117
|
+
if (looksBinary(buf))
|
|
118
|
+
return null;
|
|
119
|
+
return buf.toString('utf8').replace(/^\uFEFF/, '');
|
|
120
|
+
}
|
|
121
|
+
export function randomId(bytes = 2) {
|
|
122
|
+
return randomBytes(bytes).toString('hex');
|
|
123
|
+
}
|
|
124
|
+
export async function listFilesRecursive(dir, maxFiles = 5000) {
|
|
125
|
+
const out = [];
|
|
126
|
+
async function walk(d) {
|
|
127
|
+
let entries;
|
|
128
|
+
try {
|
|
129
|
+
entries = await fs.readdir(d, { withFileTypes: true });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
for (const e of entries) {
|
|
135
|
+
if (out.length >= maxFiles)
|
|
136
|
+
return;
|
|
137
|
+
const full = path.join(d, e.name);
|
|
138
|
+
if (e.isDirectory())
|
|
139
|
+
await walk(full);
|
|
140
|
+
else if (e.isFile())
|
|
141
|
+
out.push(full);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
await walk(dir);
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/** Atomically write text only when it differs from what is on disk. Returns true if a write happened. */
|
|
148
|
+
export async function writeTextIfChanged(file, text) {
|
|
149
|
+
try {
|
|
150
|
+
if ((await fs.readFile(file, 'utf8')) === text)
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* missing: write below */
|
|
155
|
+
}
|
|
156
|
+
await atomicWriteFile(file, text);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
export async function writeJsonIfChanged(file, value) {
|
|
160
|
+
const text = JSON.stringify(value, null, 2) + '\n';
|
|
161
|
+
JSON.parse(text);
|
|
162
|
+
return writeTextIfChanged(file, text);
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=fs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/utils/fs.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs, createReadStream } from 'node:fs';\nimport { createHash, randomBytes } from 'node:crypto';\nimport path from 'node:path';\nimport { DevWorkspaceError } from '../errors/index.js';\n\nexport const MAX_JSON_BYTES = 5 * 1024 * 1024;\n\nexport async function pathExists(p: string): Promise<boolean> {\n try {\n await fs.access(p);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function ensureDir(p: string): Promise<void> {\n await fs.mkdir(p, { recursive: true });\n}\n\nasync function renameWithRetry(from: string, to: string): Promise<void> {\n let lastErr: unknown;\n // Windows briefly denies renames while another process (reader, indexer, editor file watcher) holds the target.\n for (let i = 0; i < 20; i++) {\n try {\n await fs.rename(from, to);\n return;\n } catch (e) {\n lastErr = e;\n const code = (e as NodeJS.ErrnoException).code;\n if (code !== 'EPERM' && code !== 'EBUSY' && code !== 'EACCES') break;\n await new Promise((r) => setTimeout(r, Math.min(25 * (i + 1), 150)));\n }\n }\n throw lastErr;\n}\n\n/** Write to a temp file in the same directory, flush it, then atomically rename into place. */\nexport async function atomicWriteFile(file: string, data: string | Uint8Array, validate?: (data: string) => void): Promise<void> {\n await ensureDir(path.dirname(file));\n if (validate && typeof data === 'string') validate(data);\n const tmp = `${file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;\n let fh;\n try {\n fh = await fs.open(tmp, 'wx');\n await fh.writeFile(data);\n await fh.sync();\n await fh.close();\n fh = undefined;\n await renameWithRetry(tmp, file);\n } catch (e) {\n if (fh) await fh.close().catch(() => undefined);\n await fs.rm(tmp, { force: true }).catch(() => undefined);\n throw new DevWorkspaceError('IO', `Could not write ${path.basename(file)}.`, {\n cause: e,\n hint: 'Check file permissions and free disk space.',\n });\n }\n}\n\nexport async function atomicWriteJson(file: string, value: unknown): Promise<void> {\n const text = JSON.stringify(value, null, 2) + '\\n';\n await atomicWriteFile(file, text, (t) => {\n JSON.parse(t);\n });\n}\n\nexport async function readJsonFile(file: string, maxBytes = MAX_JSON_BYTES): Promise<unknown> {\n let stat;\n try {\n stat = await fs.stat(file);\n } catch (e) {\n throw new DevWorkspaceError('NOT_FOUND', `File not found: ${path.basename(file)}`, { cause: e });\n }\n if (stat.size > maxBytes) {\n throw new DevWorkspaceError('SCHEMA', `${path.basename(file)} is unreasonably large (${stat.size} bytes).`, {\n hint: 'Restore it from version control or a backup in .devworkspace/local/backups.',\n });\n }\n const text = await fs.readFile(file, 'utf8');\n try {\n return JSON.parse(text.replace(/^\\uFEFF/, ''));\n } catch (e) {\n throw new DevWorkspaceError('SCHEMA', `${path.basename(file)} contains malformed JSON.`, {\n cause: e,\n hint: 'Restore the file from version control (git checkout -- <file>) or from .devworkspace/local/backups.',\n });\n }\n}\n\nexport async function sha256File(file: string): Promise<string> {\n const hash = createHash('sha256');\n await new Promise<void>((resolve, reject) => {\n const s = createReadStream(file);\n s.on('data', (c) => hash.update(c));\n s.on('error', reject);\n s.on('end', () => resolve());\n });\n return hash.digest('hex');\n}\n\nexport function sha256Text(text: string): string {\n return createHash('sha256').update(text).digest('hex');\n}\n\nexport function looksBinary(buf: Uint8Array): boolean {\n const n = Math.min(buf.length, 8000);\n for (let i = 0; i < n; i++) if (buf[i] === 0) return true;\n return false;\n}\n\n/** Reads a text file; returns null if binary or larger than maxBytes. */\nexport async function readTextBounded(file: string, maxBytes = 512 * 1024): Promise<string | null> {\n const stat = await fs.stat(file);\n if (stat.size > maxBytes) return null;\n const buf = await fs.readFile(file);\n if (looksBinary(buf)) return null;\n return buf.toString('utf8').replace(/^\\uFEFF/, '');\n}\n\nexport function randomId(bytes = 2): string {\n return randomBytes(bytes).toString('hex');\n}\n\nexport async function listFilesRecursive(dir: string, maxFiles = 5000): Promise<string[]> {\n const out: string[] = [];\n async function walk(d: string): Promise<void> {\n let entries;\n try {\n entries = await fs.readdir(d, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n if (out.length >= maxFiles) return;\n const full = path.join(d, e.name);\n if (e.isDirectory()) await walk(full);\n else if (e.isFile()) out.push(full);\n }\n }\n await walk(dir);\n return out;\n}\n\n/** Atomically write text only when it differs from what is on disk. Returns true if a write happened. */\nexport async function writeTextIfChanged(file: string, text: string): Promise<boolean> {\n try {\n if ((await fs.readFile(file, 'utf8')) === text) return false;\n } catch {\n /* missing: write below */\n }\n await atomicWriteFile(file, text);\n return true;\n}\n\nexport async function writeJsonIfChanged(file: string, value: unknown): Promise<boolean> {\n const text = JSON.stringify(value, null, 2) + '\\n';\n JSON.parse(text);\n return writeTextIfChanged(file, text);\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface LockOptions {
|
|
2
|
+
timeoutMs?: number;
|
|
3
|
+
staleMs?: number;
|
|
4
|
+
}
|
|
5
|
+
/** Exclusive advisory lock stored under .devworkspace/local/. Stale locks (dead pid or too old) are broken. */
|
|
6
|
+
export declare function withLock<T>(workspaceDir: string, fn: () => Promise<T>, opts?: LockOptions): Promise<T>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { promises as fs, unlinkSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DevWorkspaceError } from '../errors/index.js';
|
|
4
|
+
import { ensureDir } from './fs.js';
|
|
5
|
+
import { onCleanup } from './cleanup.js';
|
|
6
|
+
function pidAlive(pid) {
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
return e.code === 'EPERM';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Exclusive advisory lock stored under .devworkspace/local/. Stale locks (dead pid or too old) are broken. */
|
|
16
|
+
export async function withLock(workspaceDir, fn, opts = {}) {
|
|
17
|
+
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
18
|
+
const staleMs = opts.staleMs ?? 60_000;
|
|
19
|
+
const dir = path.join(workspaceDir, 'local');
|
|
20
|
+
const lockPath = path.join(dir, 'workspace.lock');
|
|
21
|
+
await ensureDir(dir);
|
|
22
|
+
const started = Date.now();
|
|
23
|
+
for (;;) {
|
|
24
|
+
try {
|
|
25
|
+
const fh = await fs.open(lockPath, 'wx');
|
|
26
|
+
await fh.writeFile(JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
27
|
+
await fh.close();
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
if (e.code !== 'EEXIST') {
|
|
32
|
+
throw new DevWorkspaceError('IO', 'Could not acquire the workspace lock.', { cause: e });
|
|
33
|
+
}
|
|
34
|
+
let stale;
|
|
35
|
+
try {
|
|
36
|
+
const info = JSON.parse(await fs.readFile(lockPath, 'utf8'));
|
|
37
|
+
const age = Date.now() - (info.at ?? 0);
|
|
38
|
+
stale = age > staleMs || (typeof info.pid === 'number' && !pidAlive(info.pid));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Unreadable lock: another process may be mid-write, so only break it once it is old.
|
|
42
|
+
try {
|
|
43
|
+
stale = Date.now() - (await fs.stat(lockPath)).mtimeMs > 2000;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
stale = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (stale) {
|
|
50
|
+
await fs.rm(lockPath, { force: true });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (Date.now() - started > timeoutMs) {
|
|
54
|
+
throw new DevWorkspaceError('LOCKED', 'Another DevWorkspace process is modifying this workspace.', {
|
|
55
|
+
hint: `Wait for it to finish, or delete ${path.join('.devworkspace', 'local', 'workspace.lock')} if no process is running.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const dispose = onCleanup(() => {
|
|
62
|
+
try {
|
|
63
|
+
unlinkSync(lockPath);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* already gone */
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
return await fn();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
dispose();
|
|
74
|
+
await fs.rm(lockPath, { force: true }).catch(() => undefined);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/utils/lock.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs, unlinkSync } from 'node:fs';\nimport path from 'node:path';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport { ensureDir } from './fs.js';\nimport { onCleanup } from './cleanup.js';\n\nexport interface LockOptions {\n timeoutMs?: number;\n staleMs?: number;\n}\n\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n return (e as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/** Exclusive advisory lock stored under .devworkspace/local/. Stale locks (dead pid or too old) are broken. */\nexport async function withLock<T>(workspaceDir: string, fn: () => Promise<T>, opts: LockOptions = {}): Promise<T> {\n const timeoutMs = opts.timeoutMs ?? 5000;\n const staleMs = opts.staleMs ?? 60_000;\n const dir = path.join(workspaceDir, 'local');\n const lockPath = path.join(dir, 'workspace.lock');\n await ensureDir(dir);\n const started = Date.now();\n for (;;) {\n try {\n const fh = await fs.open(lockPath, 'wx');\n await fh.writeFile(JSON.stringify({ pid: process.pid, at: Date.now() }));\n await fh.close();\n break;\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new DevWorkspaceError('IO', 'Could not acquire the workspace lock.', { cause: e });\n }\n let stale: boolean;\n try {\n const info = JSON.parse(await fs.readFile(lockPath, 'utf8')) as { pid?: number; at?: number };\n const age = Date.now() - (info.at ?? 0);\n stale = age > staleMs || (typeof info.pid === 'number' && !pidAlive(info.pid));\n } catch {\n // Unreadable lock: another process may be mid-write, so only break it once it is old.\n try {\n stale = Date.now() - (await fs.stat(lockPath)).mtimeMs > 2000;\n } catch {\n stale = true;\n }\n }\n if (stale) {\n await fs.rm(lockPath, { force: true });\n continue;\n }\n if (Date.now() - started > timeoutMs) {\n throw new DevWorkspaceError('LOCKED', 'Another DevWorkspace process is modifying this workspace.', {\n hint: `Wait for it to finish, or delete ${path.join('.devworkspace', 'local', 'workspace.lock')} if no process is running.`,\n });\n }\n await new Promise((r) => setTimeout(r, 50));\n }\n }\n const dispose = onCleanup(() => {\n try {\n unlinkSync(lockPath);\n } catch {\n /* already gone */\n }\n });\n try {\n return await fn();\n } finally {\n dispose();\n await fs.rm(lockPath, { force: true }).catch(() => undefined);\n }\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
2
|
+
export interface Logger {
|
|
3
|
+
level: LogLevel;
|
|
4
|
+
error(msg: string): void;
|
|
5
|
+
warn(msg: string): void;
|
|
6
|
+
info(msg: string): void;
|
|
7
|
+
debug(msg: string): void;
|
|
8
|
+
}
|
|
9
|
+
/** Every message is passed through secret redaction before it is written. */
|
|
10
|
+
export declare function createLogger(level: LogLevel, write?: (line: string) => void): Logger;
|
|
11
|
+
export declare const silentLogger: Logger;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { redact } from '../security/redact.js';
|
|
2
|
+
const ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
3
|
+
/** Every message is passed through secret redaction before it is written. */
|
|
4
|
+
export function createLogger(level, write = (l) => process.stderr.write(l + '\n')) {
|
|
5
|
+
const emit = (l, msg) => {
|
|
6
|
+
if (ORDER[l] <= ORDER[level])
|
|
7
|
+
write(l === 'info' ? redact(msg) : `[${l}] ${redact(msg)}`);
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
level,
|
|
11
|
+
error: (m) => emit('error', m),
|
|
12
|
+
warn: (m) => emit('warn', m),
|
|
13
|
+
info: (m) => emit('info', m),
|
|
14
|
+
debug: (m) => emit('debug', m),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export const silentLogger = createLogger('error', () => undefined);
|
|
18
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAG/C,MAAM,KAAK,GAA6B,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;AAUjF,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAAC,KAAe,EAAE,QAAgC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC;IACjH,MAAM,IAAI,GAAG,CAAC,CAAW,EAAE,GAAW,EAAQ,EAAE;QAC9C,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC;YAAE,KAAK,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC5F,CAAC,CAAC;IACF,OAAO;QACL,KAAK;QACL,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC9B,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAC5B,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAC5B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;KAC/B,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAW,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC","sourcesContent":["import { redact } from '../security/redact.js';\n\nexport type LogLevel = 'error' | 'warn' | 'info' | 'debug';\nconst ORDER: Record<LogLevel, number> = { error: 0, warn: 1, info: 2, debug: 3 };\n\nexport interface Logger {\n level: LogLevel;\n error(msg: string): void;\n warn(msg: string): void;\n info(msg: string): void;\n debug(msg: string): void;\n}\n\n/** Every message is passed through secret redaction before it is written. */\nexport function createLogger(level: LogLevel, write: (line: string) => void = (l) => process.stderr.write(l + '\\n')): Logger {\n const emit = (l: LogLevel, msg: string): void => {\n if (ORDER[l] <= ORDER[level]) write(l === 'info' ? redact(msg) : `[${l}] ${redact(msg)}`);\n };\n return {\n level,\n error: (m) => emit('error', m),\n warn: (m) => emit('warn', m),\n info: (m) => emit('info', m),\n debug: (m) => emit('debug', m),\n };\n}\n\nexport const silentLogger: Logger = createLogger('error', () => undefined);\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
type PathMod = typeof path.posix;
|
|
3
|
+
/** Normalize any path flavour to forward slashes without duplicate separators. */
|
|
4
|
+
export declare function toPosix(p: string): string;
|
|
5
|
+
export declare function looksAbsolute(p: string): boolean;
|
|
6
|
+
/** True if target equals root or lives beneath it. Pass path.win32 / path.posix to test either flavour. */
|
|
7
|
+
export declare function isInside(root: string, target: string, mod?: PathMod): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Validate a portable, project-relative path (as stored in knowledge or archives).
|
|
10
|
+
* Returns an error string, or null if the path is acceptable.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateRelativePath(rel: string): string | null;
|
|
13
|
+
export declare function resolveInside(root: string, rel: string, mod?: PathMod): string;
|
|
14
|
+
/** Fails if the deepest existing ancestor of target resolves (through symlinks) outside root. */
|
|
15
|
+
export declare function assertNoSymlinkEscape(root: string, target: string): Promise<void>;
|
|
16
|
+
/** Detects machine-specific absolute paths that must not appear in portable knowledge. */
|
|
17
|
+
export declare function findAbsolutePaths(text: string): string[];
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { DevWorkspaceError } from '../errors/index.js';
|
|
4
|
+
/** Normalize any path flavour to forward slashes without duplicate separators. */
|
|
5
|
+
export function toPosix(p) {
|
|
6
|
+
return p.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
|
7
|
+
}
|
|
8
|
+
export function looksAbsolute(p) {
|
|
9
|
+
return /^[a-zA-Z]:[\\/]/.test(p) || p.startsWith('\\\\') || p.startsWith('/') || p.startsWith('\\');
|
|
10
|
+
}
|
|
11
|
+
/** True if target equals root or lives beneath it. Pass path.win32 / path.posix to test either flavour. */
|
|
12
|
+
export function isInside(root, target, mod = path) {
|
|
13
|
+
const rel = mod.relative(root, target);
|
|
14
|
+
if (rel === '')
|
|
15
|
+
return true;
|
|
16
|
+
if (mod.isAbsolute(rel))
|
|
17
|
+
return false;
|
|
18
|
+
return rel !== '..' && !rel.startsWith('..' + mod.sep) && !rel.startsWith('../');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Validate a portable, project-relative path (as stored in knowledge or archives).
|
|
22
|
+
* Returns an error string, or null if the path is acceptable.
|
|
23
|
+
*/
|
|
24
|
+
export function validateRelativePath(rel) {
|
|
25
|
+
if (rel.length === 0)
|
|
26
|
+
return 'empty path';
|
|
27
|
+
if (rel.length > 240)
|
|
28
|
+
return 'path too long';
|
|
29
|
+
if (rel.includes('\0'))
|
|
30
|
+
return 'NUL byte in path';
|
|
31
|
+
if (rel.includes('\\'))
|
|
32
|
+
return 'backslash in portable path';
|
|
33
|
+
if (looksAbsolute(rel))
|
|
34
|
+
return 'absolute path';
|
|
35
|
+
for (const s of rel.split('/')) {
|
|
36
|
+
if (s === '')
|
|
37
|
+
return 'empty path segment';
|
|
38
|
+
if (s === '..')
|
|
39
|
+
return 'parent traversal';
|
|
40
|
+
if (s === '.')
|
|
41
|
+
return 'dot segment';
|
|
42
|
+
// eslint-disable-next-line no-control-regex
|
|
43
|
+
if (/[<>:"|?*\u0000-\u001f]/.test(s))
|
|
44
|
+
return 'characters not portable to Windows';
|
|
45
|
+
if (/[. ]$/.test(s))
|
|
46
|
+
return 'segment ends with dot or space';
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
export function resolveInside(root, rel, mod = path) {
|
|
51
|
+
const posix = toPosix(rel);
|
|
52
|
+
const err = validateRelativePath(posix);
|
|
53
|
+
if (err) {
|
|
54
|
+
throw new DevWorkspaceError('UNSAFE_PATH', `Refusing unsafe path "${rel}" (${err}).`, {
|
|
55
|
+
hint: 'Use paths relative to the project root without ".." segments.',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const full = mod.resolve(root, ...posix.split('/'));
|
|
59
|
+
if (!isInside(root, full, mod)) {
|
|
60
|
+
throw new DevWorkspaceError('UNSAFE_PATH', `Path "${rel}" escapes the project root.`);
|
|
61
|
+
}
|
|
62
|
+
return full;
|
|
63
|
+
}
|
|
64
|
+
/** Fails if the deepest existing ancestor of target resolves (through symlinks) outside root. */
|
|
65
|
+
export async function assertNoSymlinkEscape(root, target) {
|
|
66
|
+
const realRoot = await fs.realpath(root);
|
|
67
|
+
let probe = target;
|
|
68
|
+
for (;;) {
|
|
69
|
+
let real;
|
|
70
|
+
try {
|
|
71
|
+
real = await fs.realpath(probe);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
real = undefined;
|
|
75
|
+
}
|
|
76
|
+
if (real !== undefined) {
|
|
77
|
+
if (!isInside(realRoot, real)) {
|
|
78
|
+
throw new DevWorkspaceError('UNSAFE_PATH', `Path resolves outside the project through a symlink: ${path.basename(target)}`, {
|
|
79
|
+
hint: 'Remove the symlink or choose a path inside the project.',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const parent = path.dirname(probe);
|
|
85
|
+
if (parent === probe)
|
|
86
|
+
return;
|
|
87
|
+
probe = parent;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Detects machine-specific absolute paths that must not appear in portable knowledge. */
|
|
91
|
+
export function findAbsolutePaths(text) {
|
|
92
|
+
const hits = text.match(/(?:\b[A-Za-z]:\\[^\s"'`<>|]+|\/(?:Users|home)\/[^\s"'`<>|]+)/g);
|
|
93
|
+
return hits ? [...new Set(hits)] : [];
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\nimport { promises as fs } from 'node:fs';\nimport { DevWorkspaceError } from '../errors/index.js';\n\ntype PathMod = typeof path.posix;\n\n/** Normalize any path flavour to forward slashes without duplicate separators. */\nexport function toPosix(p: string): string {\n return p.replace(/\\\\/g, '/').replace(/\\/{2,}/g, '/');\n}\n\nexport function looksAbsolute(p: string): boolean {\n return /^[a-zA-Z]:[\\\\/]/.test(p) || p.startsWith('\\\\\\\\') || p.startsWith('/') || p.startsWith('\\\\');\n}\n\n/** True if target equals root or lives beneath it. Pass path.win32 / path.posix to test either flavour. */\nexport function isInside(root: string, target: string, mod: PathMod = path): boolean {\n const rel = mod.relative(root, target);\n if (rel === '') return true;\n if (mod.isAbsolute(rel)) return false;\n return rel !== '..' && !rel.startsWith('..' + mod.sep) && !rel.startsWith('../');\n}\n\n/**\n * Validate a portable, project-relative path (as stored in knowledge or archives).\n * Returns an error string, or null if the path is acceptable.\n */\nexport function validateRelativePath(rel: string): string | null {\n if (rel.length === 0) return 'empty path';\n if (rel.length > 240) return 'path too long';\n if (rel.includes('\\0')) return 'NUL byte in path';\n if (rel.includes('\\\\')) return 'backslash in portable path';\n if (looksAbsolute(rel)) return 'absolute path';\n for (const s of rel.split('/')) {\n if (s === '') return 'empty path segment';\n if (s === '..') return 'parent traversal';\n if (s === '.') return 'dot segment';\n // eslint-disable-next-line no-control-regex\n if (/[<>:\"|?*\\u0000-\\u001f]/.test(s)) return 'characters not portable to Windows';\n if (/[. ]$/.test(s)) return 'segment ends with dot or space';\n }\n return null;\n}\n\nexport function resolveInside(root: string, rel: string, mod: PathMod = path): string {\n const posix = toPosix(rel);\n const err = validateRelativePath(posix);\n if (err) {\n throw new DevWorkspaceError('UNSAFE_PATH', `Refusing unsafe path \"${rel}\" (${err}).`, {\n hint: 'Use paths relative to the project root without \"..\" segments.',\n });\n }\n const full = mod.resolve(root, ...posix.split('/'));\n if (!isInside(root, full, mod)) {\n throw new DevWorkspaceError('UNSAFE_PATH', `Path \"${rel}\" escapes the project root.`);\n }\n return full;\n}\n\n/** Fails if the deepest existing ancestor of target resolves (through symlinks) outside root. */\nexport async function assertNoSymlinkEscape(root: string, target: string): Promise<void> {\n const realRoot = await fs.realpath(root);\n let probe = target;\n for (;;) {\n let real: string | undefined;\n try {\n real = await fs.realpath(probe);\n } catch {\n real = undefined;\n }\n if (real !== undefined) {\n if (!isInside(realRoot, real)) {\n throw new DevWorkspaceError('UNSAFE_PATH', `Path resolves outside the project through a symlink: ${path.basename(target)}`, {\n hint: 'Remove the symlink or choose a path inside the project.',\n });\n }\n return;\n }\n const parent = path.dirname(probe);\n if (parent === probe) return;\n probe = parent;\n }\n}\n\n/** Detects machine-specific absolute paths that must not appear in portable knowledge. */\nexport function findAbsolutePaths(text: string): string[] {\n const hits = text.match(/(?:\\b[A-Za-z]:\\\\[^\\s\"'`<>|]+|\\/(?:Users|home)\\/[^\\s\"'`<>|]+)/g);\n return hits ? [...new Set(hits)] : [];\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface RunResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
code: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
timedOut: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RunOptions {
|
|
10
|
+
cwd?: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
maxBytes?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Run an executable with an argument array. Never uses a shell, so project- or
|
|
16
|
+
* user-supplied values cannot be interpreted as shell syntax.
|
|
17
|
+
*/
|
|
18
|
+
export declare function run(command: string, args: string[], opts?: RunOptions): Promise<RunResult>;
|
|
19
|
+
/** Locate an executable on PATH (honouring PATHEXT on Windows) without executing anything. */
|
|
20
|
+
export declare function which(name: string, env?: NodeJS.ProcessEnv): Promise<string | null>;
|
|
21
|
+
/** .cmd/.bat shims need a shell on Windows; we refuse to execute them. */
|
|
22
|
+
export declare function isShellShim(file: string): boolean;
|