ultra-builder-pro-cli 0.25.1 → 0.26.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/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/contexts/TEMPLATE.md +94 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +107 -1
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +328 -0
- package/bin/delegate.cjs +463 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +125 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +77 -87
- package/docs/PHILOSOPHY.md +208 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +65 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +140 -155
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +97 -189
- package/hooks/README.md +19 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +201 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +115 -50
- package/skills/ultra-change/references/change-contract.md +68 -3
- package/skills/ultra-delegate/SKILL.md +89 -0
- package/skills/ultra-delegate/references/delegation-contract.md +59 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +98 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +115 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +118 -0
- package/skills/ultra-grilling/references/reframing.md +72 -0
- package/skills/ultra-init/SKILL.md +102 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +101 -68
- package/skills/ultra-research/SKILL.md +68 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +84 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +75 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1714
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- package/ultra-tools/commands/task.cjs +0 -105
|
@@ -1,1021 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Implementation for MCP tool `task.init_project` and the CLI fallback
|
|
4
|
-
// `ultra-tools task init-project`. Copies the bundled templates/.ultra/ skeleton
|
|
5
|
-
// to the target project root and records project metadata in authoritative state.
|
|
6
|
-
//
|
|
7
|
-
// Error contract matches spec/mcp-tools.yaml#task.init_project:
|
|
8
|
-
// ULTRA_DIR_EXISTS | TEMPLATE_MISSING | TARGET_NOT_DIR | IO_ERROR
|
|
9
|
-
//
|
|
10
|
-
// Not a pure function — touches the filesystem. Kept in lib/ for parity with
|
|
11
|
-
// state-ops/projector modules; Phase 4+ may move cross-tool helpers to
|
|
12
|
-
// mcp-server/tools/ if the catalogue grows.
|
|
13
|
-
|
|
14
|
-
const fs = require('node:fs');
|
|
15
|
-
const path = require('node:path');
|
|
16
|
-
const Database = require('better-sqlite3');
|
|
17
|
-
|
|
18
|
-
const { EXPECTED_VERSION, initStateDb } = require('./state-db.cjs');
|
|
19
|
-
const { assertStateAuthority, inspectProjection } = require('./state-authority.cjs');
|
|
20
|
-
const taskLedger = require('./task-ledger.cjs');
|
|
21
|
-
const baselines = require('./baseline-workflow.cjs');
|
|
22
|
-
const gitBootstrap = require('./git-bootstrap.cjs');
|
|
23
|
-
const runtimePaths = require('./runtime-paths.cjs');
|
|
24
|
-
const runtimeState = require('./runtime-state.cjs');
|
|
25
|
-
const ops = require('./state-ops.cjs');
|
|
26
|
-
const stageCheckpoints = require('./stage-checkpoints.cjs');
|
|
27
|
-
|
|
28
|
-
const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
29
|
-
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
30
|
-
: path.resolve(__dirname, '..', '..');
|
|
31
|
-
const SOURCE_TEMPLATE = path.join(REPO_ROOT, 'templates', '.ultra');
|
|
32
|
-
const PACKAGED_TEMPLATE = path.join(REPO_ROOT, '.ultra-template');
|
|
33
|
-
const DEFAULT_TEMPLATE = fs.existsSync(SOURCE_TEMPLATE) ? SOURCE_TEMPLATE : PACKAGED_TEMPLATE;
|
|
34
|
-
|
|
35
|
-
class InitProjectError extends Error {
|
|
36
|
-
constructor(code, message, { retriable = false, cause, details } = {}) {
|
|
37
|
-
super(message);
|
|
38
|
-
this.name = 'InitProjectError';
|
|
39
|
-
this.code = code;
|
|
40
|
-
this.retriable = retriable;
|
|
41
|
-
if (cause) this.cause = cause;
|
|
42
|
-
if (details) this.details = details;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function resolveTargetDir(target) {
|
|
47
|
-
if (!target) throw new InitProjectError('TARGET_NOT_DIR', 'target_dir must be a non-empty string');
|
|
48
|
-
return path.isAbsolute(target) ? target : path.resolve(process.cwd(), target);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function ensureTargetDir(absTarget) {
|
|
52
|
-
if (!fs.existsSync(absTarget)) {
|
|
53
|
-
try { fs.mkdirSync(absTarget, { recursive: true }); }
|
|
54
|
-
catch (err) { throw new InitProjectError('IO_ERROR', `cannot create target_dir: ${err.message}`, { retriable: true, cause: err }); }
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const stat = fs.statSync(absTarget);
|
|
58
|
-
if (!stat.isDirectory()) {
|
|
59
|
-
throw new InitProjectError('TARGET_NOT_DIR', `target_dir exists but is not a directory: ${absTarget}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function ensureTemplate(sourceOverride) {
|
|
64
|
-
const source = sourceOverride ? path.resolve(sourceOverride) : DEFAULT_TEMPLATE;
|
|
65
|
-
if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) {
|
|
66
|
-
throw new InitProjectError('TEMPLATE_MISSING', `source_template not found: ${source}`);
|
|
67
|
-
}
|
|
68
|
-
return source;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function timestampSlug(date = new Date()) {
|
|
72
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
73
|
-
return [
|
|
74
|
-
date.getFullYear(),
|
|
75
|
-
pad(date.getMonth() + 1),
|
|
76
|
-
pad(date.getDate()),
|
|
77
|
-
].join('') + '-' + [
|
|
78
|
-
pad(date.getHours()),
|
|
79
|
-
pad(date.getMinutes()),
|
|
80
|
-
pad(date.getSeconds()),
|
|
81
|
-
].join('');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function walkRelative(root) {
|
|
85
|
-
const out = [];
|
|
86
|
-
function walk(dir) {
|
|
87
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
88
|
-
if (entry.name === '.DS_Store') continue;
|
|
89
|
-
const full = path.join(dir, entry.name);
|
|
90
|
-
const rel = path.relative(root, full);
|
|
91
|
-
if (entry.isDirectory()) walk(full);
|
|
92
|
-
else if (entry.isFile()) out.push(rel);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
walk(root);
|
|
96
|
-
return out.sort();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function copyTemplate(source, dest) {
|
|
100
|
-
const files = walkRelative(source);
|
|
101
|
-
for (const rel of files) {
|
|
102
|
-
const from = path.join(source, rel);
|
|
103
|
-
const to = path.join(dest, rel);
|
|
104
|
-
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
105
|
-
fs.copyFileSync(from, to);
|
|
106
|
-
}
|
|
107
|
-
return files;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function copyMissingTemplate(source, dest) {
|
|
111
|
-
const copied = [];
|
|
112
|
-
for (const rel of walkRelative(source)) {
|
|
113
|
-
const to = path.join(dest, rel);
|
|
114
|
-
if (fs.existsSync(to)) continue;
|
|
115
|
-
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
116
|
-
fs.copyFileSync(path.join(source, rel), to);
|
|
117
|
-
copied.push(rel);
|
|
118
|
-
}
|
|
119
|
-
return copied;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function validateTasksTemplate(ultraDir) {
|
|
123
|
-
const tasksFile = path.join(ultraDir, 'tasks', 'tasks.json');
|
|
124
|
-
if (!fs.existsSync(tasksFile)) return;
|
|
125
|
-
let data;
|
|
126
|
-
try { data = JSON.parse(fs.readFileSync(tasksFile, 'utf8')); }
|
|
127
|
-
catch (err) { throw new InitProjectError('IO_ERROR', `tasks.json malformed: ${err.message}`, { cause: err }); }
|
|
128
|
-
if (!data || typeof data !== 'object' || !Array.isArray(data.tasks)) {
|
|
129
|
-
throw new InitProjectError('IO_ERROR', 'tasks.json template must contain a tasks array');
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const IGNORED_ADOPTION_ENTRIES = new Set([
|
|
134
|
-
'.DS_Store', '.git', '.gitignore', '.ultra', 'node_modules', 'vendor', '.venv', 'venv',
|
|
135
|
-
]);
|
|
136
|
-
|
|
137
|
-
const IGNORED_SCAN_DIRS = new Set([
|
|
138
|
-
...IGNORED_ADOPTION_ENTRIES, 'dist', 'build', 'coverage', '.next', '.turbo', 'target',
|
|
139
|
-
]);
|
|
140
|
-
const MANIFEST_NAMES = new Set([
|
|
141
|
-
'package.json', 'pyproject.toml', 'requirements.txt', 'Cargo.toml', 'go.mod', 'go.work',
|
|
142
|
-
'pom.xml', 'build.gradle', 'build.gradle.kts', 'Gemfile', 'composer.json',
|
|
143
|
-
]);
|
|
144
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
145
|
-
'.astro', '.c', '.cc', '.cpp', '.cs', '.css', '.dart', '.ex', '.exs', '.fs', '.fsx',
|
|
146
|
-
'.go', '.graphql', '.gql', '.hcl', '.html', '.java', '.js', '.jsx', '.kt', '.kts',
|
|
147
|
-
'.less', '.lua', '.mjs', '.move', '.php', '.proto', '.py', '.r', '.R', '.rb', '.rs',
|
|
148
|
-
'.sass', '.scala', '.scss', '.sh', '.sol', '.svelte', '.swift', '.tf', '.ts', '.tsx',
|
|
149
|
-
'.vue',
|
|
150
|
-
]);
|
|
151
|
-
function scanRepositoryFiles(rootDir, { maxFiles = 5000, maxDepth = 8 } = {}) {
|
|
152
|
-
const files = [];
|
|
153
|
-
function walk(dir, depth) {
|
|
154
|
-
if (files.length >= maxFiles || depth > maxDepth) return;
|
|
155
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
156
|
-
if (files.length >= maxFiles) break;
|
|
157
|
-
if (entry.name === '.DS_Store') continue;
|
|
158
|
-
const full = path.join(dir, entry.name);
|
|
159
|
-
const rel = path.relative(rootDir, full).split(path.sep).join('/');
|
|
160
|
-
if (entry.isDirectory()) {
|
|
161
|
-
if (!IGNORED_SCAN_DIRS.has(entry.name) && !entry.name.startsWith('.ultra.backup.')) {
|
|
162
|
-
walk(full, depth + 1);
|
|
163
|
-
}
|
|
164
|
-
} else if (entry.isFile()) files.push(rel);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
walk(rootDir, 0);
|
|
168
|
-
return { files: files.sort(), truncated: files.length >= maxFiles };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function expandWorkspacePattern(rootDir, pattern) {
|
|
172
|
-
const normalized = String(pattern || '').trim().replace(/^['"]|['"]$/g, '').replace(/\\/g, '/');
|
|
173
|
-
if (!normalized) return [];
|
|
174
|
-
if (!normalized.includes('*')) {
|
|
175
|
-
const candidate = path.join(rootDir, normalized);
|
|
176
|
-
return fs.existsSync(candidate) && fs.statSync(candidate).isDirectory() ? [normalized] : [];
|
|
177
|
-
}
|
|
178
|
-
if (!normalized.endsWith('/*') || normalized.slice(0, -2).includes('*')) return [];
|
|
179
|
-
const parentRel = normalized.slice(0, -2);
|
|
180
|
-
const parent = path.join(rootDir, parentRel);
|
|
181
|
-
if (!fs.existsSync(parent) || !fs.statSync(parent).isDirectory()) return [];
|
|
182
|
-
return fs.readdirSync(parent, { withFileTypes: true })
|
|
183
|
-
.filter((entry) => entry.isDirectory())
|
|
184
|
-
.map((entry) => `${parentRel}/${entry.name}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function packageProfile(rootDir) {
|
|
188
|
-
const file = path.join(rootDir, 'package.json');
|
|
189
|
-
if (!fs.existsSync(file)) return { workspacePatterns: [], verificationCommands: [] };
|
|
190
|
-
try {
|
|
191
|
-
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
192
|
-
const rawWorkspaces = Array.isArray(pkg.workspaces)
|
|
193
|
-
? pkg.workspaces
|
|
194
|
-
: (Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : []);
|
|
195
|
-
const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
196
|
-
const verificationCommands = ['build', 'test', 'lint', 'typecheck']
|
|
197
|
-
.filter((name) => typeof scripts[name] === 'string' && scripts[name].trim())
|
|
198
|
-
.map((name) => name === 'test' ? 'npm test' : `npm run ${name}`);
|
|
199
|
-
return { workspacePatterns: rawWorkspaces, verificationCommands };
|
|
200
|
-
} catch {
|
|
201
|
-
return { workspacePatterns: [], verificationCommands: [] };
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const JAVASCRIPT_TECHNOLOGIES = Object.freeze([
|
|
206
|
-
{ packages: ['next'], name: 'Next.js', capability: 'frontend' },
|
|
207
|
-
{ packages: ['react'], name: 'React', capability: 'frontend' },
|
|
208
|
-
{ packages: ['vue', 'nuxt'], name: 'Vue', capability: 'frontend' },
|
|
209
|
-
{ packages: ['svelte', '@sveltejs/kit'], name: 'Svelte', capability: 'frontend' },
|
|
210
|
-
{ packages: ['@angular/core'], name: 'Angular', capability: 'frontend' },
|
|
211
|
-
{ packages: ['express'], name: 'Express', capability: 'backend' },
|
|
212
|
-
{ packages: ['fastify'], name: 'Fastify', capability: 'backend' },
|
|
213
|
-
{ packages: ['@nestjs/core'], name: 'NestJS', capability: 'backend' },
|
|
214
|
-
{ packages: ['hono'], name: 'Hono', capability: 'backend' },
|
|
215
|
-
{ packages: ['koa'], name: 'Koa', capability: 'backend' },
|
|
216
|
-
{ packages: ['commander', 'yargs', 'oclif'], name: 'Node CLI', capability: 'cli' },
|
|
217
|
-
{ packages: ['@prisma/client', 'prisma'], name: 'Prisma' },
|
|
218
|
-
{ packages: ['typescript'], name: 'TypeScript' },
|
|
219
|
-
{ packages: ['vitest'], name: 'Vitest' },
|
|
220
|
-
{ packages: ['jest'], name: 'Jest' },
|
|
221
|
-
{ packages: ['playwright', '@playwright/test'], name: 'Playwright' },
|
|
222
|
-
]);
|
|
223
|
-
|
|
224
|
-
function readTextBounded(file, maxBytes = 256 * 1024) {
|
|
225
|
-
try {
|
|
226
|
-
const stat = fs.statSync(file);
|
|
227
|
-
if (!stat.isFile() || stat.size > maxBytes) return '';
|
|
228
|
-
return fs.readFileSync(file, 'utf8');
|
|
229
|
-
} catch {
|
|
230
|
-
return '';
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function detectTechnologyProfile(rootDir, files) {
|
|
235
|
-
const technologies = new Set();
|
|
236
|
-
const capabilities = new Set();
|
|
237
|
-
const packageFiles = files.filter((file) => path.basename(file) === 'package.json').slice(0, 100);
|
|
238
|
-
for (const relative of packageFiles) {
|
|
239
|
-
let pkg;
|
|
240
|
-
try { pkg = JSON.parse(readTextBounded(path.join(rootDir, relative))); }
|
|
241
|
-
catch { continue; }
|
|
242
|
-
technologies.add('Node.js');
|
|
243
|
-
const dependencies = new Set([
|
|
244
|
-
...Object.keys(pkg.dependencies || {}),
|
|
245
|
-
...Object.keys(pkg.devDependencies || {}),
|
|
246
|
-
...Object.keys(pkg.peerDependencies || {}),
|
|
247
|
-
]);
|
|
248
|
-
if (pkg.bin && (typeof pkg.bin === 'string' || typeof pkg.bin === 'object')) {
|
|
249
|
-
capabilities.add('cli');
|
|
250
|
-
}
|
|
251
|
-
for (const technology of JAVASCRIPT_TECHNOLOGIES) {
|
|
252
|
-
if (!technology.packages.some((name) => dependencies.has(name))) continue;
|
|
253
|
-
technologies.add(technology.name);
|
|
254
|
-
if (technology.capability) capabilities.add(technology.capability);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (files.some((file) => file === 'tsconfig.json' || /\.(ts|tsx)$/.test(file))) {
|
|
259
|
-
technologies.add('TypeScript');
|
|
260
|
-
}
|
|
261
|
-
if (files.some((file) => /(^|\/)index\.html$/.test(file))) {
|
|
262
|
-
technologies.add('HTML');
|
|
263
|
-
capabilities.add('frontend');
|
|
264
|
-
}
|
|
265
|
-
if (files.some((file) => /\.(css|scss|sass|less)$/.test(file))) technologies.add('CSS');
|
|
266
|
-
|
|
267
|
-
const pythonFiles = files.filter((file) => (
|
|
268
|
-
['pyproject.toml', 'requirements.txt'].includes(path.basename(file)) || file.endsWith('.py')
|
|
269
|
-
));
|
|
270
|
-
if (pythonFiles.length > 0) {
|
|
271
|
-
technologies.add('Python');
|
|
272
|
-
const declarations = pythonFiles
|
|
273
|
-
.filter((file) => ['pyproject.toml', 'requirements.txt'].includes(path.basename(file)))
|
|
274
|
-
.map((file) => readTextBounded(path.join(rootDir, file)).toLowerCase())
|
|
275
|
-
.join('\n');
|
|
276
|
-
for (const [needle, label, capability] of [
|
|
277
|
-
['fastapi', 'FastAPI', 'backend'], ['django', 'Django', 'backend'],
|
|
278
|
-
['flask', 'Flask', 'backend'], ['typer', 'Typer', 'cli'], ['click', 'Click', 'cli'],
|
|
279
|
-
['pytest', 'Pytest', null],
|
|
280
|
-
]) {
|
|
281
|
-
if (!declarations.includes(needle)) continue;
|
|
282
|
-
technologies.add(label);
|
|
283
|
-
if (capability) capabilities.add(capability);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (files.some((file) => ['go.mod', 'go.work'].includes(path.basename(file)) || file.endsWith('.go'))) {
|
|
287
|
-
technologies.add('Go');
|
|
288
|
-
}
|
|
289
|
-
if (files.some((file) => path.basename(file) === 'Cargo.toml' || file.endsWith('.rs'))) {
|
|
290
|
-
technologies.add('Rust');
|
|
291
|
-
const cargo = files.filter((file) => path.basename(file) === 'Cargo.toml')
|
|
292
|
-
.map((file) => readTextBounded(path.join(rootDir, file)).toLowerCase()).join('\n');
|
|
293
|
-
if (cargo.includes('clap')) {
|
|
294
|
-
technologies.add('Clap');
|
|
295
|
-
capabilities.add('cli');
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (files.some((file) => ['pom.xml', 'build.gradle', 'build.gradle.kts'].includes(path.basename(file)))) {
|
|
299
|
-
technologies.add('JVM');
|
|
300
|
-
}
|
|
301
|
-
if (files.some((file) => path.basename(file) === 'Gemfile' || file.endsWith('.rb'))) {
|
|
302
|
-
technologies.add('Ruby');
|
|
303
|
-
}
|
|
304
|
-
if (files.some((file) => path.basename(file) === 'composer.json' || file.endsWith('.php'))) {
|
|
305
|
-
technologies.add('PHP');
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
let projectType = 'other';
|
|
309
|
-
if (capabilities.has('frontend') && capabilities.has('backend')) projectType = 'fullstack';
|
|
310
|
-
else if (capabilities.has('frontend')) projectType = 'web';
|
|
311
|
-
else if (capabilities.has('backend')) projectType = 'api';
|
|
312
|
-
else if (capabilities.has('cli')) projectType = 'cli';
|
|
313
|
-
return {
|
|
314
|
-
detected_project_type: projectType,
|
|
315
|
-
detected_stack: technologies.size > 0 ? [...technologies].join(', ') : null,
|
|
316
|
-
technology_signals: [...technologies],
|
|
317
|
-
capability_signals: [...capabilities].sort(),
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function classifyRepository(absTarget) {
|
|
322
|
-
const { files, truncated } = scanRepositoryFiles(absTarget);
|
|
323
|
-
const packageData = packageProfile(absTarget);
|
|
324
|
-
const manifestSignals = files.filter((file) => MANIFEST_NAMES.has(path.basename(file))).slice(0, 100);
|
|
325
|
-
const testSignals = files.filter((file) => {
|
|
326
|
-
const lower = file.toLowerCase();
|
|
327
|
-
return /(^|\/)(test|tests|__tests__|spec)(\/|$)/.test(lower)
|
|
328
|
-
|| /\.(test|spec)\.[^.]+$/.test(lower);
|
|
329
|
-
}).slice(0, 100);
|
|
330
|
-
const deploymentSignals = files.filter((file) => {
|
|
331
|
-
const lower = file.toLowerCase();
|
|
332
|
-
return /(^|\/)(dockerfile|railway\.json|railway\.toml|vercel\.json|fly\.toml)$/.test(lower)
|
|
333
|
-
|| lower.startsWith('.github/workflows/')
|
|
334
|
-
|| /(^|\/)docker-compose[^/]*\.ya?ml$/.test(lower);
|
|
335
|
-
}).slice(0, 100);
|
|
336
|
-
const databaseSignals = files.filter((file) => {
|
|
337
|
-
const lower = file.toLowerCase();
|
|
338
|
-
return /(^|\/)(migrations?|prisma|schema)(\/|$)/.test(lower)
|
|
339
|
-
|| /(^|\/)schema\.(sql|prisma)$/.test(lower);
|
|
340
|
-
}).slice(0, 100);
|
|
341
|
-
const sourceSignals = files.filter((file) => {
|
|
342
|
-
const lower = file.toLowerCase();
|
|
343
|
-
if (testSignals.includes(file) || /(^|\/)[^/]+\.config\.(c|m)?(j|t)s$/.test(lower)) return false;
|
|
344
|
-
const ext = path.extname(file);
|
|
345
|
-
if (!SOURCE_EXTENSIONS.has(ext)) return false;
|
|
346
|
-
return true;
|
|
347
|
-
}).slice(0, 200);
|
|
348
|
-
const workspaceRoots = [...new Set(packageData.workspacePatterns
|
|
349
|
-
.flatMap((pattern) => expandWorkspacePattern(absTarget, pattern)))]
|
|
350
|
-
.sort();
|
|
351
|
-
const workspaceMarkers = [];
|
|
352
|
-
if (packageData.workspacePatterns.length > 0) workspaceMarkers.push('package.json#workspaces');
|
|
353
|
-
for (const marker of ['pnpm-workspace.yaml', 'lerna.json', 'nx.json', 'turbo.json', 'go.work']) {
|
|
354
|
-
if (files.includes(marker)) workspaceMarkers.push(marker);
|
|
355
|
-
}
|
|
356
|
-
if (workspaceRoots.length === 0 && files.some((file) => /^packages\/[^/]+\/package\.json$/.test(file))) {
|
|
357
|
-
workspaceRoots.push(...files
|
|
358
|
-
.filter((file) => /^packages\/[^/]+\/package\.json$/.test(file))
|
|
359
|
-
.map((file) => path.posix.dirname(file)));
|
|
360
|
-
workspaceRoots.sort();
|
|
361
|
-
}
|
|
362
|
-
const reasons = [];
|
|
363
|
-
if (sourceSignals.length > 0) reasons.push('SOURCE_PRESENT');
|
|
364
|
-
if (testSignals.length > 0) reasons.push('TESTS_PRESENT');
|
|
365
|
-
if (deploymentSignals.length > 0) reasons.push('DEPLOYMENT_PRESENT');
|
|
366
|
-
if (databaseSignals.length > 0) reasons.push('PERSISTED_STATE_PRESENT');
|
|
367
|
-
if (workspaceMarkers.length > 0 || workspaceRoots.length > 0) reasons.push('MONOREPO_PRESENT');
|
|
368
|
-
if (truncated) reasons.push('SCAN_TRUNCATED');
|
|
369
|
-
if (reasons.length === 0) reasons.push('SKELETON_ONLY');
|
|
370
|
-
const mode = sourceSignals.length > 0 || testSignals.length > 0
|
|
371
|
-
|| deploymentSignals.length > 0 || databaseSignals.length > 0 || truncated
|
|
372
|
-
? 'brownfield' : 'greenfield';
|
|
373
|
-
const technologyProfile = detectTechnologyProfile(absTarget, files);
|
|
374
|
-
return {
|
|
375
|
-
mode,
|
|
376
|
-
...technologyProfile,
|
|
377
|
-
reasons,
|
|
378
|
-
repository_kind: workspaceMarkers.length > 0 || workspaceRoots.length > 0 ? 'monorepo' : 'single',
|
|
379
|
-
workspace_markers: workspaceMarkers,
|
|
380
|
-
workspace_roots: [...new Set(workspaceRoots)],
|
|
381
|
-
manifest_signals: manifestSignals,
|
|
382
|
-
source_signals: sourceSignals,
|
|
383
|
-
test_signals: testSignals,
|
|
384
|
-
deployment_signals: deploymentSignals,
|
|
385
|
-
database_signals: databaseSignals,
|
|
386
|
-
verification_commands: packageData.verificationCommands,
|
|
387
|
-
scan_truncated: truncated,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function detectProjectMode(absTarget, requested = 'auto') {
|
|
392
|
-
if (!['auto', 'greenfield', 'brownfield'].includes(requested)) {
|
|
393
|
-
throw new InitProjectError('VALIDATION_ERROR', `unsupported initialization mode: ${requested}`);
|
|
394
|
-
}
|
|
395
|
-
if (requested !== 'auto') return requested;
|
|
396
|
-
return classifyRepository(absTarget).mode;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function rollbackInitialization(ultraDir, backupPath) {
|
|
400
|
-
const errors = [];
|
|
401
|
-
try {
|
|
402
|
-
if (fs.existsSync(ultraDir)) fs.rmSync(ultraDir, { recursive: true, force: true });
|
|
403
|
-
} catch (error) {
|
|
404
|
-
errors.push(`partial cleanup failed: ${error.message}`);
|
|
405
|
-
}
|
|
406
|
-
if (backupPath) {
|
|
407
|
-
try { fs.renameSync(backupPath, ultraDir); }
|
|
408
|
-
catch (error) { errors.push(`backup restore failed: ${error.message}`); }
|
|
409
|
-
}
|
|
410
|
-
return errors;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function createResumeSnapshot(ultraDir, stateDbPath) {
|
|
414
|
-
const token = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
415
|
-
const runtimeDir = path.dirname(stateDbPath);
|
|
416
|
-
const statePath = path.join(runtimeDir, `.resume-state-${token}.db`);
|
|
417
|
-
const projectionDir = path.join(runtimeDir, `.resume-projections-${token}`);
|
|
418
|
-
const projectionTargets = [
|
|
419
|
-
path.join('tasks', 'tasks.json'),
|
|
420
|
-
path.join('tasks', 'contexts'),
|
|
421
|
-
path.join('.runtime', 'projections'),
|
|
422
|
-
path.join('.runtime', 'backups', 'task-ledger'),
|
|
423
|
-
];
|
|
424
|
-
let db;
|
|
425
|
-
try {
|
|
426
|
-
db = new Database(stateDbPath, { fileMustExist: true });
|
|
427
|
-
db.prepare('VACUUM INTO ?').run(statePath);
|
|
428
|
-
db.close();
|
|
429
|
-
db = null;
|
|
430
|
-
const projections = projectionTargets.map((relative) => {
|
|
431
|
-
const source = path.join(ultraDir, relative);
|
|
432
|
-
const existed = fs.existsSync(source);
|
|
433
|
-
if (existed) {
|
|
434
|
-
const destination = path.join(projectionDir, relative);
|
|
435
|
-
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
436
|
-
fs.cpSync(source, destination, { recursive: true });
|
|
437
|
-
}
|
|
438
|
-
return { relative, existed };
|
|
439
|
-
});
|
|
440
|
-
return { statePath, projectionDir, projections, stateDbPath, ultraDir };
|
|
441
|
-
} catch (error) {
|
|
442
|
-
if (db?.open) db.close();
|
|
443
|
-
for (const target of [statePath, projectionDir]) {
|
|
444
|
-
try { fs.rmSync(target, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
445
|
-
}
|
|
446
|
-
throw error;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function discardResumeSnapshot(snapshot) {
|
|
451
|
-
if (!snapshot) return [];
|
|
452
|
-
const errors = [];
|
|
453
|
-
for (const target of [snapshot.statePath, snapshot.projectionDir]) {
|
|
454
|
-
try { fs.rmSync(target, { recursive: true, force: true }); }
|
|
455
|
-
catch (error) { errors.push(`resume snapshot cleanup failed for ${target}: ${error.message}`); }
|
|
456
|
-
}
|
|
457
|
-
return errors;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function restoreResumeSnapshot(snapshot, {
|
|
461
|
-
renameSync = fs.renameSync,
|
|
462
|
-
} = {}) {
|
|
463
|
-
if (!snapshot) return [];
|
|
464
|
-
const errors = [];
|
|
465
|
-
const runtimeDir = path.dirname(snapshot.stateDbPath);
|
|
466
|
-
const liveStage = path.join(
|
|
467
|
-
runtimeDir,
|
|
468
|
-
`.resume-live-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
469
|
-
);
|
|
470
|
-
const movedLive = [];
|
|
471
|
-
try {
|
|
472
|
-
fs.mkdirSync(liveStage, { recursive: false });
|
|
473
|
-
for (const suffix of ['', '-wal', '-shm']) {
|
|
474
|
-
const live = `${snapshot.stateDbPath}${suffix}`;
|
|
475
|
-
if (!fs.existsSync(live)) continue;
|
|
476
|
-
const staged = path.join(liveStage, `state.db${suffix}`);
|
|
477
|
-
renameSync(live, staged);
|
|
478
|
-
movedLive.push({ live, staged });
|
|
479
|
-
}
|
|
480
|
-
renameSync(snapshot.statePath, snapshot.stateDbPath);
|
|
481
|
-
} catch (error) {
|
|
482
|
-
errors.push(`state.db restore failed: ${error.message}`);
|
|
483
|
-
for (const item of [...movedLive].reverse()) {
|
|
484
|
-
try {
|
|
485
|
-
if (fs.existsSync(item.live)) {
|
|
486
|
-
errors.push(`live state rollback target already exists: ${item.live}`);
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
renameSync(item.staged, item.live);
|
|
490
|
-
} catch (rollbackError) {
|
|
491
|
-
errors.push(`live state rollback failed for ${item.live}: ${rollbackError.message}`);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
// The snapshot and staged live image are recovery evidence. Never destroy
|
|
495
|
-
// either when publication or rollback is incomplete.
|
|
496
|
-
try {
|
|
497
|
-
if (fs.existsSync(liveStage) && fs.readdirSync(liveStage).length === 0) {
|
|
498
|
-
fs.rmdirSync(liveStage);
|
|
499
|
-
}
|
|
500
|
-
} catch (cleanupError) {
|
|
501
|
-
errors.push(`live state staging cleanup failed: ${cleanupError.message}`);
|
|
502
|
-
}
|
|
503
|
-
return errors;
|
|
504
|
-
}
|
|
505
|
-
try {
|
|
506
|
-
fs.rmSync(liveStage, { recursive: true, force: true });
|
|
507
|
-
} catch (error) {
|
|
508
|
-
errors.push(`prior live state cleanup failed: ${error.message}`);
|
|
509
|
-
}
|
|
510
|
-
for (const item of snapshot.projections) {
|
|
511
|
-
const target = path.join(snapshot.ultraDir, item.relative);
|
|
512
|
-
try {
|
|
513
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
514
|
-
if (item.existed) {
|
|
515
|
-
const source = path.join(snapshot.projectionDir, item.relative);
|
|
516
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
517
|
-
fs.cpSync(source, target, { recursive: true });
|
|
518
|
-
}
|
|
519
|
-
} catch (error) {
|
|
520
|
-
errors.push(`projection restore failed for ${item.relative}: ${error.message}`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
if (errors.length === 0) {
|
|
524
|
-
errors.push(...discardResumeSnapshot(snapshot));
|
|
525
|
-
}
|
|
526
|
-
return errors;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function projectionMigrationError(projection) {
|
|
530
|
-
const targets = { '4.4': '4.5', '4.5': EXPECTED_VERSION };
|
|
531
|
-
const target = targets[projection.version];
|
|
532
|
-
if (target) {
|
|
533
|
-
return new InitProjectError(
|
|
534
|
-
'LEGACY_STATE_MIGRATION_REQUIRED',
|
|
535
|
-
`state.db has no tasks but ${projection.path} contains ${projection.taskCount} v${projection.version} tasks; `
|
|
536
|
-
+ `run ultra-tools migrate --from=${projection.version} --to=${target} --source-dir <project-root> before resuming initialization`,
|
|
537
|
-
{ details: { projection_path: projection.path, projection_version: projection.version, legacy_task_count: projection.taskCount } },
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
return new InitProjectError(
|
|
541
|
-
'STATE_AUTHORITY_CONFLICT',
|
|
542
|
-
`state.db has no tasks but ${projection.path} contains ${projection.taskCount} tasks with unsupported projection version ${projection.version}`,
|
|
543
|
-
{ details: { projection_path: projection.path, projection_version: projection.version, legacy_task_count: projection.taskCount } },
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function ensureInitializationCheckpoint(db, baseline) {
|
|
548
|
-
let initCheckpoint = stageCheckpoints.currentCheckpoint(
|
|
549
|
-
db,
|
|
550
|
-
'init',
|
|
551
|
-
{ baseline_id: baseline.id },
|
|
552
|
-
{ includeDraft: false },
|
|
553
|
-
);
|
|
554
|
-
if (!initCheckpoint) {
|
|
555
|
-
const draft = stageCheckpoints.saveDraft(db, {
|
|
556
|
-
stage: 'init',
|
|
557
|
-
scope: { baseline_id: baseline.id },
|
|
558
|
-
payload: {
|
|
559
|
-
project_name: baseline.project_name,
|
|
560
|
-
repository_revision: baseline.repository_revision,
|
|
561
|
-
scope: baseline.scope,
|
|
562
|
-
},
|
|
563
|
-
evidence: [{
|
|
564
|
-
kind: 'authority',
|
|
565
|
-
ref: '.ultra/.runtime/state.db',
|
|
566
|
-
summary: 'State authority, scaffold, projection, and repository classification were verified.',
|
|
567
|
-
}],
|
|
568
|
-
diagnostics: [],
|
|
569
|
-
idempotency_key: `init:${baseline.id}:draft`,
|
|
570
|
-
});
|
|
571
|
-
initCheckpoint = stageCheckpoints.acceptDraft(db, {
|
|
572
|
-
id: draft.id,
|
|
573
|
-
idempotency_key: `init:${baseline.id}:accept`,
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
let researchCheckpoint = stageCheckpoints.currentCheckpoint(
|
|
577
|
-
db,
|
|
578
|
-
'research',
|
|
579
|
-
{ baseline_id: baseline.id },
|
|
580
|
-
{ includeDraft: false },
|
|
581
|
-
);
|
|
582
|
-
const legacyResearch = db.prepare(
|
|
583
|
-
`SELECT id, mode, status FROM workflow_runs
|
|
584
|
-
WHERE kind = 'research' AND baseline_id = ?
|
|
585
|
-
ORDER BY started_at DESC, rowid DESC LIMIT 1`,
|
|
586
|
-
).get(baseline.id);
|
|
587
|
-
if (!researchCheckpoint && legacyResearch?.status === 'completed') {
|
|
588
|
-
const draft = stageCheckpoints.saveDraft(db, {
|
|
589
|
-
stage: 'research',
|
|
590
|
-
scope: { baseline_id: baseline.id },
|
|
591
|
-
payload: {
|
|
592
|
-
migration_source: 'workflow_runs',
|
|
593
|
-
legacy_run_id: legacyResearch.id,
|
|
594
|
-
mode: legacyResearch.mode,
|
|
595
|
-
},
|
|
596
|
-
evidence: [{
|
|
597
|
-
kind: 'legacy_workflow',
|
|
598
|
-
ref: legacyResearch.id,
|
|
599
|
-
summary: 'Completed legacy research provenance migrated during project resume.',
|
|
600
|
-
}],
|
|
601
|
-
diagnostics: [],
|
|
602
|
-
idempotency_key: `research:${baseline.id}:legacy:${legacyResearch.id}:draft`,
|
|
603
|
-
});
|
|
604
|
-
researchCheckpoint = stageCheckpoints.acceptDraft(db, {
|
|
605
|
-
id: draft.id,
|
|
606
|
-
idempotency_key: `research:${baseline.id}:legacy:${legacyResearch.id}:accept`,
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
const compatibilityOnly = baseline.mode === 'migrated';
|
|
610
|
-
const provenanceComplete = !compatibilityOnly
|
|
611
|
-
&& baseline.status === 'ready'
|
|
612
|
-
&& initCheckpoint.status === 'accepted'
|
|
613
|
-
&& researchCheckpoint?.status === 'accepted';
|
|
614
|
-
const readyWithoutProvenance = baseline.status === 'ready' && !provenanceComplete;
|
|
615
|
-
const allowedTransitions = provenanceComplete
|
|
616
|
-
? ['ultra-change', 'ultra-research', 'ultra-status', 'ultra-doctor']
|
|
617
|
-
: (readyWithoutProvenance
|
|
618
|
-
? ['ultra-doctor', 'ultra-research', 'ultra-status']
|
|
619
|
-
: ['ultra-research', 'ultra-status', 'ultra-doctor']);
|
|
620
|
-
return {
|
|
621
|
-
init: {
|
|
622
|
-
id: initCheckpoint.id,
|
|
623
|
-
status: initCheckpoint.status,
|
|
624
|
-
revision: initCheckpoint.revision,
|
|
625
|
-
digest: initCheckpoint.digest,
|
|
626
|
-
},
|
|
627
|
-
research: researchCheckpoint ? {
|
|
628
|
-
id: researchCheckpoint.id,
|
|
629
|
-
status: researchCheckpoint.status,
|
|
630
|
-
revision: researchCheckpoint.revision,
|
|
631
|
-
digest: researchCheckpoint.digest,
|
|
632
|
-
mode: researchCheckpoint.payload?.mode || null,
|
|
633
|
-
} : null,
|
|
634
|
-
provenance_status: compatibilityOnly
|
|
635
|
-
? 'compatibility_only'
|
|
636
|
-
: (provenanceComplete ? 'complete' : (readyWithoutProvenance ? 'incomplete' : 'pending_research')),
|
|
637
|
-
allowed_transitions: allowedTransitions,
|
|
638
|
-
required_transition: readyWithoutProvenance
|
|
639
|
-
? 'ultra-doctor'
|
|
640
|
-
: (compatibilityOnly ? 'ultra-research' : null),
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function resumeProject({
|
|
645
|
-
absTarget, ultraDir, source, project_name, project_type, stack, resolvedMode,
|
|
646
|
-
scope, repositoryProfile, projection, git_mode,
|
|
647
|
-
}) {
|
|
648
|
-
let locatedStatePath;
|
|
649
|
-
try {
|
|
650
|
-
locatedStatePath = runtimePaths.locateStateDb(absTarget, { env: {} });
|
|
651
|
-
} catch (error) {
|
|
652
|
-
throw new InitProjectError(
|
|
653
|
-
error.code || 'RUNTIME_STATE_CONFLICT',
|
|
654
|
-
error.message,
|
|
655
|
-
{ cause: error, details: error.details },
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
const stateExisted = fs.existsSync(locatedStatePath);
|
|
659
|
-
let runtimeLayout;
|
|
660
|
-
let gitSetup;
|
|
661
|
-
const stateDbPath = runtimePaths.pathsFor(absTarget).stateDbPath;
|
|
662
|
-
let initialized;
|
|
663
|
-
let copiedFiles = [];
|
|
664
|
-
let baselineResult;
|
|
665
|
-
let checkpointResult;
|
|
666
|
-
let teamCheckpoint;
|
|
667
|
-
let resumeSnapshot;
|
|
668
|
-
const completeResume = () => {
|
|
669
|
-
try {
|
|
670
|
-
if (stateExisted) {
|
|
671
|
-
// Snapshot the exact entry authority before initStateDb can migrate its
|
|
672
|
-
// schema. A failed snapshot therefore cannot partially upgrade state.
|
|
673
|
-
resumeSnapshot = createResumeSnapshot(ultraDir, stateDbPath);
|
|
674
|
-
}
|
|
675
|
-
initialized = initStateDb(stateDbPath);
|
|
676
|
-
const { db } = initialized;
|
|
677
|
-
if (projection && (
|
|
678
|
-
projection.taskCount > 0 || projection.kind === taskLedger.LEDGER_KIND
|
|
679
|
-
)) {
|
|
680
|
-
teamCheckpoint = assertStateAuthority(db, absTarget, { importTeamLedger: true });
|
|
681
|
-
}
|
|
682
|
-
copiedFiles = copyMissingTemplate(source, ultraDir);
|
|
683
|
-
validateTasksTemplate(ultraDir);
|
|
684
|
-
repositoryProfile.git = gitSetup.result;
|
|
685
|
-
baselineResult = baselines.readBaseline(db);
|
|
686
|
-
if (!baselineResult) {
|
|
687
|
-
baselineResult = baselines.startBaseline(db, {
|
|
688
|
-
id: 'project-baseline', project_name, project_type, stack,
|
|
689
|
-
mode: resolvedMode, repository_revision: baselines.gitHead(absTarget),
|
|
690
|
-
scope: scope === undefined ? ['.'] : scope,
|
|
691
|
-
classification: repositoryProfile,
|
|
692
|
-
}, { rootDir: absTarget });
|
|
693
|
-
} else {
|
|
694
|
-
baselineResult = baselines.refreshInProgressBaseline(db, {
|
|
695
|
-
id: baselineResult.id,
|
|
696
|
-
project_name,
|
|
697
|
-
project_type,
|
|
698
|
-
stack,
|
|
699
|
-
classification: repositoryProfile,
|
|
700
|
-
}, { rootDir: absTarget });
|
|
701
|
-
}
|
|
702
|
-
checkpointResult = ensureInitializationCheckpoint(db, baselineResult);
|
|
703
|
-
if (projection?.kind === 'legacy-task-projection') {
|
|
704
|
-
const publication = taskLedger.publishTaskLedger(db, {
|
|
705
|
-
rootDir: absTarget,
|
|
706
|
-
reason: 'legacy_projection_upgraded',
|
|
707
|
-
});
|
|
708
|
-
teamCheckpoint = {
|
|
709
|
-
generation: publication.ledger.generation,
|
|
710
|
-
state_digest: publication.ledger.state_digest,
|
|
711
|
-
imported: 0,
|
|
712
|
-
deleted: 0,
|
|
713
|
-
imported_changes: 0,
|
|
714
|
-
imported_baseline: false,
|
|
715
|
-
requires_plan_revalidation: false,
|
|
716
|
-
requires_baseline_revalidation: false,
|
|
717
|
-
already_current: true,
|
|
718
|
-
published: publication.changed,
|
|
719
|
-
migrated_legacy_projection: Boolean(publication.migrated_legacy_projection),
|
|
720
|
-
legacy_backup_path: publication.legacy_backup_path || null,
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
ops.appendEvent(db, {
|
|
724
|
-
type: 'project_resumed',
|
|
725
|
-
payload: {
|
|
726
|
-
project_name, mode: baselineResult.mode, baseline_id: baselineResult.id,
|
|
727
|
-
installed_missing_files: copiedFiles.length, git: gitSetup.result,
|
|
728
|
-
},
|
|
729
|
-
});
|
|
730
|
-
const job = runtimeState.enqueueProjection(db, { tool_name: 'task.init_project' });
|
|
731
|
-
const projectionResult = runtimeState.processProjectionJobs(
|
|
732
|
-
db, { rootDir: absTarget, limit: 1 },
|
|
733
|
-
);
|
|
734
|
-
const projected = projectionResult.jobs.find((item) => item.id === job.id);
|
|
735
|
-
if (!projected || projected.status !== 'completed') {
|
|
736
|
-
throw new Error(
|
|
737
|
-
`resume projection did not complete: ${projected?.error || 'projection job missing'}`,
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
} catch (error) {
|
|
741
|
-
if (initialized?.db?.open) initialized.db.close();
|
|
742
|
-
const stateRollbackErrors = stateExisted
|
|
743
|
-
? restoreResumeSnapshot(resumeSnapshot)
|
|
744
|
-
: [];
|
|
745
|
-
const gitRollbackErrors = gitBootstrap.rollbackGitBootstrap(
|
|
746
|
-
absTarget, gitSetup?.internal,
|
|
747
|
-
);
|
|
748
|
-
for (const rel of copiedFiles.reverse()) {
|
|
749
|
-
try { fs.rmSync(path.join(ultraDir, rel), { force: true }); } catch { /* best effort */ }
|
|
750
|
-
}
|
|
751
|
-
if (!stateExisted) {
|
|
752
|
-
for (const suffix of ['', '-wal', '-shm']) {
|
|
753
|
-
try { fs.rmSync(`${stateDbPath}${suffix}`, { force: true }); } catch { /* best effort */ }
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
const rollbackErrors = [...stateRollbackErrors, ...gitRollbackErrors];
|
|
757
|
-
if (error instanceof taskLedger.TaskLedgerError
|
|
758
|
-
|| (error.code && [
|
|
759
|
-
'LEGACY_STATE_MIGRATION_REQUIRED',
|
|
760
|
-
'STATE_AUTHORITY_CONFLICT',
|
|
761
|
-
'STATE_PROJECTION_INVALID',
|
|
762
|
-
].includes(error.code))) {
|
|
763
|
-
throw new InitProjectError(
|
|
764
|
-
error.code,
|
|
765
|
-
error.message,
|
|
766
|
-
{ cause: error, details: error.details },
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
if (error instanceof gitBootstrap.GitBootstrapError) {
|
|
770
|
-
throw new InitProjectError(
|
|
771
|
-
error.code,
|
|
772
|
-
`${error.message}${rollbackErrors.length ? `; ${rollbackErrors.join('; ')}` : ''}`,
|
|
773
|
-
{ cause: error },
|
|
774
|
-
);
|
|
775
|
-
}
|
|
776
|
-
throw new InitProjectError(
|
|
777
|
-
'IO_ERROR',
|
|
778
|
-
`project resume failed: ${error.message}; `
|
|
779
|
-
+ `${rollbackErrors.length ? 'resume rollback was incomplete' : 'existing authority and projections were restored'}`
|
|
780
|
-
+ `${rollbackErrors.length ? `; ${rollbackErrors.join('; ')}` : ''}`,
|
|
781
|
-
{ retriable: true, cause: error },
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
try {
|
|
787
|
-
runtimeLayout = runtimePaths.ensureRuntimeState(absTarget, {
|
|
788
|
-
admitStorageBoundary: () => {
|
|
789
|
-
gitSetup = gitBootstrap.bootstrapGit(absTarget, { mode: git_mode });
|
|
790
|
-
completeResume();
|
|
791
|
-
return gitSetup;
|
|
792
|
-
},
|
|
793
|
-
});
|
|
794
|
-
} catch (error) {
|
|
795
|
-
if (error instanceof InitProjectError) throw error;
|
|
796
|
-
throw new InitProjectError(
|
|
797
|
-
error.code || 'RUNTIME_STATE_MIGRATION_FAILED',
|
|
798
|
-
error.message,
|
|
799
|
-
{ cause: error, details: error.details },
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
initialized.db.close();
|
|
803
|
-
discardResumeSnapshot(resumeSnapshot);
|
|
804
|
-
const result = {
|
|
805
|
-
created_path: ultraDir,
|
|
806
|
-
state_db_path: stateDbPath,
|
|
807
|
-
status: 'resumed',
|
|
808
|
-
mode: baselineResult.mode,
|
|
809
|
-
baseline: {
|
|
810
|
-
id: baselineResult.id,
|
|
811
|
-
mode: baselineResult.mode,
|
|
812
|
-
status: baselineResult.status,
|
|
813
|
-
project_name: baselineResult.project_name,
|
|
814
|
-
project_type: baselineResult.project_type,
|
|
815
|
-
stack: baselineResult.stack,
|
|
816
|
-
repository_revision: baselineResult.repository_revision,
|
|
817
|
-
repository_branch: baselineResult.repository_branch,
|
|
818
|
-
worktree_state: baselineResult.worktree_state,
|
|
819
|
-
scope: baselineResult.scope,
|
|
820
|
-
},
|
|
821
|
-
copied_files: copiedFiles,
|
|
822
|
-
repository_profile: repositoryProfile,
|
|
823
|
-
git: gitSetup.result,
|
|
824
|
-
checkpoint: checkpointResult,
|
|
825
|
-
};
|
|
826
|
-
if (teamCheckpoint) {
|
|
827
|
-
result.team_checkpoint = {
|
|
828
|
-
generation: teamCheckpoint.generation,
|
|
829
|
-
state_digest: teamCheckpoint.state_digest,
|
|
830
|
-
imported_tasks: teamCheckpoint.imported,
|
|
831
|
-
deleted_tasks: teamCheckpoint.deleted,
|
|
832
|
-
imported_changes: teamCheckpoint.imported_changes,
|
|
833
|
-
imported_baseline: teamCheckpoint.imported_baseline,
|
|
834
|
-
requires_plan_revalidation: teamCheckpoint.requires_plan_revalidation,
|
|
835
|
-
requires_baseline_revalidation: teamCheckpoint.requires_baseline_revalidation,
|
|
836
|
-
already_current: teamCheckpoint.already_current,
|
|
837
|
-
published: Boolean(teamCheckpoint.published),
|
|
838
|
-
migrated_legacy_projection: Boolean(teamCheckpoint.migrated_legacy_projection),
|
|
839
|
-
legacy_backup_path: teamCheckpoint.legacy_backup_path || undefined,
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
if (initialized.backup_path) result.migration_backup_path = initialized.backup_path;
|
|
843
|
-
if (runtimeLayout.backupPath) {
|
|
844
|
-
result.runtime_migration_backup_path = runtimeLayout.backupPath;
|
|
845
|
-
}
|
|
846
|
-
return result;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
function initProject({
|
|
850
|
-
target_dir,
|
|
851
|
-
project_name,
|
|
852
|
-
project_type,
|
|
853
|
-
stack,
|
|
854
|
-
mode = 'auto',
|
|
855
|
-
scope,
|
|
856
|
-
resume = false,
|
|
857
|
-
overwrite = false,
|
|
858
|
-
git_mode = 'auto',
|
|
859
|
-
source_template,
|
|
860
|
-
} = {}) {
|
|
861
|
-
if (typeof project_name !== 'string' || project_name.length === 0) {
|
|
862
|
-
throw new InitProjectError('VALIDATION_ERROR', 'project_name must be non-empty');
|
|
863
|
-
}
|
|
864
|
-
if (resume && overwrite) {
|
|
865
|
-
throw new InitProjectError('VALIDATION_ERROR', 'resume and overwrite are mutually exclusive');
|
|
866
|
-
}
|
|
867
|
-
if (!gitBootstrap.GIT_MODES.has(git_mode)) {
|
|
868
|
-
throw new InitProjectError(
|
|
869
|
-
'VALIDATION_ERROR',
|
|
870
|
-
`git_mode must be one of: ${[...gitBootstrap.GIT_MODES].join(', ')}`,
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const absTarget = resolveTargetDir(target_dir);
|
|
875
|
-
ensureTargetDir(absTarget);
|
|
876
|
-
const repositoryProfile = classifyRepository(absTarget);
|
|
877
|
-
const resolvedMode = mode === 'auto' ? repositoryProfile.mode : detectProjectMode(absTarget, mode);
|
|
878
|
-
const resolvedProjectType = project_type || repositoryProfile.detected_project_type;
|
|
879
|
-
const resolvedStack = stack || repositoryProfile.detected_stack || undefined;
|
|
880
|
-
repositoryProfile.detected_mode = repositoryProfile.mode;
|
|
881
|
-
repositoryProfile.mode = resolvedMode;
|
|
882
|
-
repositoryProfile.selected_project_type = resolvedProjectType;
|
|
883
|
-
repositoryProfile.selected_stack = resolvedStack || null;
|
|
884
|
-
const source = ensureTemplate(source_template);
|
|
885
|
-
const ultraDir = path.join(absTarget, '.ultra');
|
|
886
|
-
|
|
887
|
-
if (fs.existsSync(ultraDir) && resume) {
|
|
888
|
-
let projection;
|
|
889
|
-
try { projection = inspectProjection(absTarget); }
|
|
890
|
-
catch (error) {
|
|
891
|
-
throw new InitProjectError(error.code || 'STATE_PROJECTION_INVALID', error.message, {
|
|
892
|
-
cause: error, details: error.details,
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
let locatedStatePath;
|
|
896
|
-
try {
|
|
897
|
-
locatedStatePath = runtimePaths.locateStateDb(absTarget, { env: {} });
|
|
898
|
-
} catch (error) {
|
|
899
|
-
throw new InitProjectError(
|
|
900
|
-
error.code || 'RUNTIME_STATE_CONFLICT',
|
|
901
|
-
error.message,
|
|
902
|
-
{ cause: error, details: error.details },
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
if (projection && projection.taskCount > 0
|
|
906
|
-
&& projection.kind !== taskLedger.LEDGER_KIND
|
|
907
|
-
&& !fs.existsSync(locatedStatePath)) {
|
|
908
|
-
throw projectionMigrationError(projection);
|
|
909
|
-
}
|
|
910
|
-
return resumeProject({
|
|
911
|
-
absTarget, ultraDir, source, project_name, project_type: resolvedProjectType,
|
|
912
|
-
stack: resolvedStack, resolvedMode,
|
|
913
|
-
scope, repositoryProfile, projection, git_mode,
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
let status = 'created';
|
|
918
|
-
let backupPath;
|
|
919
|
-
if (fs.existsSync(ultraDir)) {
|
|
920
|
-
if (!overwrite) {
|
|
921
|
-
throw new InitProjectError('ULTRA_DIR_EXISTS', `.ultra/ already exists at ${ultraDir}; pass overwrite=true to back up and recreate`);
|
|
922
|
-
}
|
|
923
|
-
backupPath = path.join(absTarget, `.ultra.backup.${timestampSlug()}`);
|
|
924
|
-
try { fs.renameSync(ultraDir, backupPath); }
|
|
925
|
-
catch (err) { throw new InitProjectError('IO_ERROR', `backup rename failed: ${err.message}`, { retriable: true, cause: err }); }
|
|
926
|
-
status = 'overwritten';
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
let copiedFiles;
|
|
930
|
-
let baselineResult;
|
|
931
|
-
let checkpointResult;
|
|
932
|
-
let gitSetup;
|
|
933
|
-
try {
|
|
934
|
-
fs.mkdirSync(ultraDir, { recursive: true });
|
|
935
|
-
copiedFiles = copyTemplate(source, ultraDir);
|
|
936
|
-
validateTasksTemplate(ultraDir);
|
|
937
|
-
gitSetup = gitBootstrap.bootstrapGit(absTarget, { mode: git_mode });
|
|
938
|
-
repositoryProfile.git = gitSetup.result;
|
|
939
|
-
const runtimeLayout = runtimePaths.ensureRuntimeState(absTarget);
|
|
940
|
-
const stateDbPath = runtimeLayout.stateDbPath;
|
|
941
|
-
const { db } = initStateDb(stateDbPath);
|
|
942
|
-
try {
|
|
943
|
-
baselineResult = baselines.startBaseline(db, {
|
|
944
|
-
id: 'project-baseline', project_name, project_type: resolvedProjectType,
|
|
945
|
-
stack: resolvedStack,
|
|
946
|
-
mode: resolvedMode, repository_revision: baselines.gitHead(absTarget),
|
|
947
|
-
scope: scope === undefined ? ['.'] : scope,
|
|
948
|
-
classification: repositoryProfile,
|
|
949
|
-
}, { rootDir: absTarget });
|
|
950
|
-
checkpointResult = ensureInitializationCheckpoint(db, baselineResult);
|
|
951
|
-
ops.appendEvent(db, {
|
|
952
|
-
type: 'project_initialized',
|
|
953
|
-
payload: {
|
|
954
|
-
project_name, mode: resolvedMode, baseline_id: baselineResult.id,
|
|
955
|
-
git: gitSetup.result,
|
|
956
|
-
},
|
|
957
|
-
});
|
|
958
|
-
const job = runtimeState.enqueueProjection(db, { tool_name: 'task.init_project' });
|
|
959
|
-
const projection = runtimeState.processProjectionJobs(db, { rootDir: absTarget, limit: 1 });
|
|
960
|
-
const projected = projection.jobs.find((item) => item.id === job.id);
|
|
961
|
-
if (!projected || projected.status !== 'completed') {
|
|
962
|
-
throw new Error(projected?.error || 'initial projection did not complete');
|
|
963
|
-
}
|
|
964
|
-
} finally {
|
|
965
|
-
db.close();
|
|
966
|
-
}
|
|
967
|
-
} catch (err) {
|
|
968
|
-
const rollbackErrors = rollbackInitialization(ultraDir, backupPath);
|
|
969
|
-
rollbackErrors.push(...gitBootstrap.rollbackGitBootstrap(absTarget, gitSetup?.internal));
|
|
970
|
-
const rollbackNote = rollbackErrors.length > 0
|
|
971
|
-
? `; rollback incomplete: ${rollbackErrors.join('; ')}`
|
|
972
|
-
: '; prior state restored';
|
|
973
|
-
if (err instanceof gitBootstrap.GitBootstrapError) {
|
|
974
|
-
throw new InitProjectError(
|
|
975
|
-
err.code,
|
|
976
|
-
`project initialization failed: ${err.message}${rollbackNote}`,
|
|
977
|
-
{ cause: err },
|
|
978
|
-
);
|
|
979
|
-
}
|
|
980
|
-
throw new InitProjectError(
|
|
981
|
-
'IO_ERROR',
|
|
982
|
-
`project initialization failed: ${err.message}${rollbackNote}`,
|
|
983
|
-
{ retriable: true, cause: err },
|
|
984
|
-
);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const stateDbPath = runtimePaths.pathsFor(absTarget).stateDbPath;
|
|
988
|
-
const result = {
|
|
989
|
-
created_path: ultraDir,
|
|
990
|
-
state_db_path: stateDbPath,
|
|
991
|
-
status,
|
|
992
|
-
mode: resolvedMode,
|
|
993
|
-
baseline: {
|
|
994
|
-
id: baselineResult.id,
|
|
995
|
-
mode: baselineResult.mode,
|
|
996
|
-
status: baselineResult.status,
|
|
997
|
-
project_name: baselineResult.project_name,
|
|
998
|
-
project_type: baselineResult.project_type,
|
|
999
|
-
stack: baselineResult.stack,
|
|
1000
|
-
repository_revision: baselineResult.repository_revision,
|
|
1001
|
-
repository_branch: baselineResult.repository_branch,
|
|
1002
|
-
worktree_state: baselineResult.worktree_state,
|
|
1003
|
-
scope: baselineResult.scope,
|
|
1004
|
-
},
|
|
1005
|
-
copied_files: copiedFiles,
|
|
1006
|
-
repository_profile: repositoryProfile,
|
|
1007
|
-
git: gitSetup.result,
|
|
1008
|
-
checkpoint: checkpointResult,
|
|
1009
|
-
};
|
|
1010
|
-
if (backupPath) result.backup_path = backupPath;
|
|
1011
|
-
return result;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
module.exports = {
|
|
1015
|
-
initProject, InitProjectError, DEFAULT_TEMPLATE, detectProjectMode, classifyRepository,
|
|
1016
|
-
_internal: {
|
|
1017
|
-
createResumeSnapshot,
|
|
1018
|
-
discardResumeSnapshot,
|
|
1019
|
-
restoreResumeSnapshot,
|
|
1020
|
-
},
|
|
1021
|
-
};
|