@aopslabs/aops 0.2.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/LICENSE +19 -0
- package/NOTICE +7 -0
- package/README.md +203 -0
- package/assets/agent-assets/core/SKILL.md +49 -0
- package/assets/agent-assets/core/references/agentspace/SKILL.md +78 -0
- package/assets/agent-assets/core/references/aops-cli-core/SKILL.md +108 -0
- package/assets/agent-assets/core/references/chatv3/SKILL.md +48 -0
- package/assets/agent-assets/core/references/collaborative-work/SKILL.md +85 -0
- package/assets/agent-assets/core/references/discuss/SKILL.md +66 -0
- package/assets/agent-assets/core/references/docman/SKILL.md +69 -0
- package/assets/agent-assets/core/references/hosted-chat/SKILL.md +64 -0
- package/assets/agent-assets/core/references/mission/SKILL.md +54 -0
- package/assets/agent-assets/core/references/projectman/SKILL.md +86 -0
- package/assets/agent-assets/core/references/sys/SKILL.md +36 -0
- package/assets/agent-assets/core/references/view/SKILL.md +58 -0
- package/assets/agent-assets/core/references/working-disciplines/SKILL.md +69 -0
- package/assets/agent-assets/core/user-guides/agent-assets.md +81 -0
- package/assets/agent-assets/core/user-guides/agentspace.md +1098 -0
- package/assets/agent-assets/core/user-guides/aops-cli.md +1107 -0
- package/assets/agent-assets/core/user-guides/aops-system.md +63 -0
- package/assets/agent-assets/core/user-guides/chatv3.md +113 -0
- package/assets/agent-assets/core/user-guides/docman.md +162 -0
- package/assets/agent-assets/core/user-guides/projectman.md +212 -0
- package/assets/agent-assets/core/user-guides/sys.md +94 -0
- package/assets/agent-assets/core/user-guides/working-disciplines.md +588 -0
- package/assets/agent-assets/gateway/aops/SKILL.md +14 -0
- package/assets/r6-remote-session-baseline.json +19 -0
- package/assets/skills/aops-install/SKILL.md +231 -0
- package/assets/skills/aops-install/agents/openai.yaml +4 -0
- package/dist/commands/activity.js +352 -0
- package/dist/commands/agent-profile.js +653 -0
- package/dist/commands/agent.js +710 -0
- package/dist/commands/agents-md.js +125 -0
- package/dist/commands/api.js +84 -0
- package/dist/commands/archive.js +873 -0
- package/dist/commands/artifact.js +535 -0
- package/dist/commands/assets.js +1414 -0
- package/dist/commands/auth/device-login.js +1 -0
- package/dist/commands/auth/legacy-export.js +177 -0
- package/dist/commands/auth/login.js +426 -0
- package/dist/commands/chat.js +1363 -0
- package/dist/commands/chatv3.js +1684 -0
- package/dist/commands/checkpoint.js +549 -0
- package/dist/commands/commercial-artifact.js +475 -0
- package/dist/commands/commercial-license.js +594 -0
- package/dist/commands/community-auth.js +62 -0
- package/dist/commands/community-cockpit.js +185 -0
- package/dist/commands/community-console.js +109 -0
- package/dist/commands/community-doctor.js +335 -0
- package/dist/commands/community-server.js +2062 -0
- package/dist/commands/community-setup.js +284 -0
- package/dist/commands/discuss.js +1448 -0
- package/dist/commands/doc.js +5219 -0
- package/dist/commands/experience.js +922 -0
- package/dist/commands/host.js +1127 -0
- package/dist/commands/init.js +118 -0
- package/dist/commands/memory.js +3129 -0
- package/dist/commands/mission.js +562 -0
- package/dist/commands/official-catalog.js +134 -0
- package/dist/commands/plan.js +84 -0
- package/dist/commands/playbook.js +415 -0
- package/dist/commands/pm/context.js +6 -0
- package/dist/commands/pm/index.js +701 -0
- package/dist/commands/pm/projectman.js +6266 -0
- package/dist/commands/project.js +1080 -0
- package/dist/commands/prompt.js +1024 -0
- package/dist/commands/repo-sync.js +1907 -0
- package/dist/commands/resource.js +527 -0
- package/dist/commands/skill.js +1211 -0
- package/dist/commands/start-disciplines.js +510 -0
- package/dist/commands/start.js +1343 -0
- package/dist/commands/target.js +309 -0
- package/dist/commands/version.js +42 -0
- package/dist/commands/view.js +1852 -0
- package/dist/launchers/cockpit.js +28 -0
- package/dist/launchers/options.js +74 -0
- package/dist/launchers/server.js +41 -0
- package/dist/lib/agent-assets/envelope.js +83 -0
- package/dist/lib/agent-assets/gateway.js +15 -0
- package/dist/lib/agent-assets/guards.js +23 -0
- package/dist/lib/agent-assets/hosted-discovery.js +148 -0
- package/dist/lib/agent-assets/hosted-package-input.js +154 -0
- package/dist/lib/agent-assets/legacy-pointer-migration.js +677 -0
- package/dist/lib/agent-assets/native-fs.js +589 -0
- package/dist/lib/agent-assets/package-manifest.js +324 -0
- package/dist/lib/agent-assets/portable-path.js +67 -0
- package/dist/lib/agent-assets/release-input.js +177 -0
- package/dist/lib/agent-assets/roots.js +45 -0
- package/dist/lib/agent-assets/runtime-binding-reader.js +545 -0
- package/dist/lib/agent-assets/runtime-targets.js +50 -0
- package/dist/lib/agent-assets/store-reader.js +1212 -0
- package/dist/lib/agent-assets/store-types.js +1 -0
- package/dist/lib/agent-assets/store-writer.js +1484 -0
- package/dist/lib/agent-assets/types.js +1 -0
- package/dist/lib/agent-assets/unicode-case-folding-v15-1.generated.js +1548 -0
- package/dist/lib/commercial-final-readiness.js +504 -0
- package/dist/lib/commercial-lifecycle-smoke-isolation.js +236 -0
- package/dist/lib/commercial-managed-postgres-docker-adapter.js +274 -0
- package/dist/lib/commercial-setup-readiness-adapter.js +179 -0
- package/dist/lib/community-client-contract.js +164 -0
- package/dist/lib/community-cockpit-lifecycle.js +601 -0
- package/dist/lib/community-command-abort.js +59 -0
- package/dist/lib/community-diagnostic.js +237 -0
- package/dist/lib/community-docker-adapter.js +498 -0
- package/dist/lib/community-home.js +31 -0
- package/dist/lib/community-instance-contract.js +148 -0
- package/dist/lib/community-instance-layout.js +97 -0
- package/dist/lib/community-lifecycle.js +1631 -0
- package/dist/lib/community-migration-snapshot.js +770 -0
- package/dist/lib/community-native-application-recovery.js +425 -0
- package/dist/lib/community-native-child.js +240 -0
- package/dist/lib/community-native-database-recovery.js +456 -0
- package/dist/lib/community-native-lifecycle.js +2458 -0
- package/dist/lib/community-native-migration.js +339 -0
- package/dist/lib/community-native-postgres.js +596 -0
- package/dist/lib/community-operation-journal.js +773 -0
- package/dist/lib/community-operation-lock.js +596 -0
- package/dist/lib/community-release-resolver.js +291 -0
- package/dist/lib/community-release-verifier.js +475 -0
- package/dist/lib/community-repo-discovery.js +252 -0
- package/dist/lib/community-setup-server-env.js +203 -0
- package/dist/lib/external-url.js +110 -0
- package/dist/lib/official-catalog-gateway.js +108 -0
- package/dist/lib/official-catalog.js +364 -0
- package/dist/lib/server-bootstrap.js +240 -0
- package/dist/lib/setup-agent-assets-bridge.js +176 -0
- package/dist/lib/setup-agent-assets-release.js +52 -0
- package/dist/lib/setup-external-postgres.js +108 -0
- package/dist/lib/setup-init-orchestrator.js +990 -0
- package/dist/lib/setup-install-guide.js +52 -0
- package/dist/lib/setup-installer-release.js +47 -0
- package/dist/lib/setup-local-postgres.js +329 -0
- package/dist/lib/setup-official-catalog-bridge.js +148 -0
- package/dist/lib/setup-readiness.js +888 -0
- package/dist/lib/tui-launcher.js +59 -0
- package/dist/main.js +150 -0
- package/dist/seeds/agents-md.js +54 -0
- package/dist/utils/agent-gateway.js +275 -0
- package/dist/utils/agents-md.js +281 -0
- package/dist/utils/api.js +255 -0
- package/dist/utils/chatv3-session-store.js +185 -0
- package/dist/utils/command.js +67 -0
- package/dist/utils/community-host-runtime.js +376 -0
- package/dist/utils/config.js +1015 -0
- package/dist/utils/discussion-workspace.js +552 -0
- package/dist/utils/experience-workspace.js +326 -0
- package/dist/utils/guide-paths.js +67 -0
- package/dist/utils/hosted-sugar.js +56 -0
- package/dist/utils/hosted-workspace.js +229 -0
- package/dist/utils/memory-workspace.js +537 -0
- package/dist/utils/operator-runtime.js +199 -0
- package/dist/utils/password-crypto.js +39 -0
- package/dist/utils/playbook-workspace.js +479 -0
- package/dist/utils/project-context.js +255 -0
- package/dist/utils/prompts.js +70 -0
- package/dist/utils/refresh-race.js +28 -0
- package/dist/utils/repo-config.js +109 -0
- package/dist/utils/repo-first-projectman.js +194 -0
- package/dist/utils/repo-first-storage.js +13 -0
- package/dist/utils/repo-first-view.js +305 -0
- package/dist/utils/session-guidance.js +278 -0
- package/dist/utils/session-state.js +137 -0
- package/dist/utils/table.js +39 -0
- package/dist/utils/transient-fs.js +49 -0
- package/dist/utils/workspace-root.js +1 -0
- package/launchers/aops-cockpit.ps1 +15 -0
- package/launchers/aops-cockpit.sh +16 -0
- package/launchers/aops-server.ps1 +15 -0
- package/launchers/aops-server.sh +16 -0
- package/native/tui/win32-x64/aops-tui.exe +0 -0
- package/package.json +81 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { logError, logSuccess } from '@aopslabs/xf-cli-ui';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { invokeHostedToolWithApiState, requireApiState, unwrapHostedToolResult, } from '../utils/agent-gateway.js';
|
|
7
|
+
import { applyCommonOptions, compactPayload, normalizeNonEmpty } from '../utils/command.js';
|
|
8
|
+
import { resolveOwnerScopeIdFromProjectRecord, resolveProjectBindingContext, } from '../utils/project-context.js';
|
|
9
|
+
import { writeFileWithRetry } from '../utils/transient-fs.js';
|
|
10
|
+
const ARCHIVE_ENTITY_TYPE = 'aops.archive.pm-graph';
|
|
11
|
+
const ARCHIVE_KIND = 'projectman.pm-graph';
|
|
12
|
+
const DEFAULT_PENDING_DOMAINS = [
|
|
13
|
+
'agentspace.memory',
|
|
14
|
+
'agentspace.discussions',
|
|
15
|
+
'agentspace.chat',
|
|
16
|
+
'hosted.prompts-skills-resources-artifacts',
|
|
17
|
+
];
|
|
18
|
+
const ARCHIVE_ENTITY_DEFS = [
|
|
19
|
+
{ key: 'boards', entityType: 'projectman.board', file: 'entities/boards.json' },
|
|
20
|
+
{ key: 'boardColumns', entityType: 'projectman.board-column', file: 'entities/board-columns.json' },
|
|
21
|
+
{ key: 'tasks', entityType: 'projectman.kanban-task', file: 'entities/kanban-tasks.json' },
|
|
22
|
+
{ key: 'sprints', entityType: 'projectman.sprint', file: 'entities/sprints.json' },
|
|
23
|
+
{ key: 'utasks', entityType: 'projectman.microtask', file: 'entities/microtasks.json' },
|
|
24
|
+
{ key: 'issues', entityType: 'projectman.issue', file: 'entities/issues.json' },
|
|
25
|
+
{ key: 'feedback', entityType: 'projectman.feedback', file: 'entities/feedback.json' },
|
|
26
|
+
{ key: 'reviewRequests', entityType: 'projectman.review-request', file: 'entities/review-requests.json' },
|
|
27
|
+
];
|
|
28
|
+
const DELETE_ORDER = [
|
|
29
|
+
{ key: 'reviewRequests', toolId: 'projectman.review-request.delete' },
|
|
30
|
+
{ key: 'feedback', toolId: 'projectman.feedback.delete' },
|
|
31
|
+
{ key: 'issues', toolId: 'projectman.issue.delete' },
|
|
32
|
+
{ key: 'utasks', toolId: 'projectman.sprint.delete-microtask' },
|
|
33
|
+
{ key: 'sprints', toolId: 'projectman.sprint.delete' },
|
|
34
|
+
{ key: 'tasks', toolId: 'projectman.kanban-task.delete' },
|
|
35
|
+
{ key: 'boardColumns', toolId: 'projectman.kanban-board-column.delete' },
|
|
36
|
+
{ key: 'columns', toolId: 'projectman.kanban-column.delete' },
|
|
37
|
+
{ key: 'boards', toolId: 'projectman.kanban-board.delete' },
|
|
38
|
+
];
|
|
39
|
+
function isRecord(value) {
|
|
40
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
function toRecordArray(value) {
|
|
43
|
+
if (Array.isArray(value))
|
|
44
|
+
return value.filter(isRecord);
|
|
45
|
+
if (!isRecord(value))
|
|
46
|
+
return [];
|
|
47
|
+
for (const candidate of [value.data, value.items, value.rows, value.results]) {
|
|
48
|
+
if (Array.isArray(candidate))
|
|
49
|
+
return candidate.filter(isRecord);
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
function extractResultData(value) {
|
|
54
|
+
if (!isRecord(value))
|
|
55
|
+
return value;
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(value, 'data'))
|
|
57
|
+
return value.data;
|
|
58
|
+
if (Object.prototype.hasOwnProperty.call(value, 'response'))
|
|
59
|
+
return extractResultData(value.response);
|
|
60
|
+
if (Object.prototype.hasOwnProperty.call(value, 'result'))
|
|
61
|
+
return extractResultData(value.result);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function numberField(value) {
|
|
65
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
66
|
+
}
|
|
67
|
+
function sortValue(value) {
|
|
68
|
+
if (Array.isArray(value))
|
|
69
|
+
return value.map(sortValue);
|
|
70
|
+
if (!isRecord(value))
|
|
71
|
+
return value;
|
|
72
|
+
return Object.keys(value)
|
|
73
|
+
.sort()
|
|
74
|
+
.reduce((acc, key) => {
|
|
75
|
+
acc[key] = sortValue(value[key]);
|
|
76
|
+
return acc;
|
|
77
|
+
}, {});
|
|
78
|
+
}
|
|
79
|
+
function canonicalJson(value) {
|
|
80
|
+
return `${JSON.stringify(sortValue(value), null, 2)}\n`;
|
|
81
|
+
}
|
|
82
|
+
function sha256(value) {
|
|
83
|
+
return createHash('sha256').update(value).digest('hex');
|
|
84
|
+
}
|
|
85
|
+
function recordId(record) {
|
|
86
|
+
return (normalizeNonEmpty(record.id) ??
|
|
87
|
+
normalizeNonEmpty(record.remoteId) ??
|
|
88
|
+
normalizeNonEmpty(record.localId) ??
|
|
89
|
+
normalizeNonEmpty(record.slug) ??
|
|
90
|
+
normalizeNonEmpty(record.name) ??
|
|
91
|
+
normalizeNonEmpty(record.title) ??
|
|
92
|
+
'');
|
|
93
|
+
}
|
|
94
|
+
function sortRecords(records) {
|
|
95
|
+
return [...records].sort((left, right) => recordId(left).localeCompare(recordId(right)));
|
|
96
|
+
}
|
|
97
|
+
function safeTimestamp(value) {
|
|
98
|
+
const raw = normalizeNonEmpty(value) ?? new Date().toISOString();
|
|
99
|
+
return raw.replace(/[^0-9A-Za-z_-]+/g, '-').replace(/^-+|-+$/g, '') || 'archive';
|
|
100
|
+
}
|
|
101
|
+
function resolveRepoRelativePath(repoRoot, maybePath) {
|
|
102
|
+
return path.isAbsolute(maybePath) ? maybePath : path.resolve(repoRoot, maybePath);
|
|
103
|
+
}
|
|
104
|
+
function archiveRoot(repoRoot, outputRoot) {
|
|
105
|
+
return resolveRepoRelativePath(repoRoot, normalizeNonEmpty(outputRoot) ?? path.join('.aops', 'archive'));
|
|
106
|
+
}
|
|
107
|
+
function buildGatewayOptions(options, context) {
|
|
108
|
+
return {
|
|
109
|
+
...options,
|
|
110
|
+
scopeId: context.scopeId,
|
|
111
|
+
projectId: context.projectId,
|
|
112
|
+
projectName: context.projectName,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function isDeletedProject(project) {
|
|
116
|
+
const status = normalizeNonEmpty(project.status)?.toLowerCase();
|
|
117
|
+
return status === 'deleted' || status === 'removed';
|
|
118
|
+
}
|
|
119
|
+
async function resolveArchiveTarget(options, fallback) {
|
|
120
|
+
const resolvedContext = await resolveProjectBindingContext({
|
|
121
|
+
...options,
|
|
122
|
+
projectSlug: normalizeNonEmpty(options.projectSlug) ?? normalizeNonEmpty(fallback?.projectSlug),
|
|
123
|
+
projectId: normalizeNonEmpty(options.projectId) ?? normalizeNonEmpty(fallback?.projectId),
|
|
124
|
+
}, { requireProject: true });
|
|
125
|
+
const apiState = await requireApiState(options);
|
|
126
|
+
if (!apiState)
|
|
127
|
+
return null;
|
|
128
|
+
const projectId = normalizeNonEmpty(resolvedContext.projectId);
|
|
129
|
+
if (!projectId)
|
|
130
|
+
throw new Error('Archive target projectId could not be resolved.');
|
|
131
|
+
const projectPayload = await invokeHostedToolWithApiState(apiState, {
|
|
132
|
+
...buildGatewayOptions(options, resolvedContext),
|
|
133
|
+
toolId: 'agentspace.project.get-by-id',
|
|
134
|
+
input: { id: projectId },
|
|
135
|
+
});
|
|
136
|
+
const project = extractResultData(unwrapHostedToolResult(projectPayload));
|
|
137
|
+
if (!isRecord(project)) {
|
|
138
|
+
throw new Error(`Archive target project "${resolvedContext.projectSlug ?? projectId}" was not found.`);
|
|
139
|
+
}
|
|
140
|
+
if (isDeletedProject(project)) {
|
|
141
|
+
throw new Error(`Archive target project "${resolvedContext.projectSlug ?? projectId}" is deleted/removed; refusing to archive.`);
|
|
142
|
+
}
|
|
143
|
+
const hydratedContext = {
|
|
144
|
+
...resolvedContext,
|
|
145
|
+
scopeId: resolveOwnerScopeIdFromProjectRecord(project, resolvedContext.scopeId),
|
|
146
|
+
projectName: normalizeNonEmpty(project.name) ?? resolvedContext.projectName,
|
|
147
|
+
projectSlug: normalizeNonEmpty(project.slug) ?? resolvedContext.projectSlug,
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
apiState,
|
|
151
|
+
resolvedContext: hydratedContext,
|
|
152
|
+
gatewayOptions: buildGatewayOptions(options, hydratedContext),
|
|
153
|
+
project,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async function invokeRead(target, toolId, input) {
|
|
157
|
+
const payload = await invokeHostedToolWithApiState(target.apiState, {
|
|
158
|
+
...target.gatewayOptions,
|
|
159
|
+
toolId,
|
|
160
|
+
input,
|
|
161
|
+
preview: false,
|
|
162
|
+
apply: false,
|
|
163
|
+
confirm: false,
|
|
164
|
+
});
|
|
165
|
+
return extractResultData(unwrapHostedToolResult(payload));
|
|
166
|
+
}
|
|
167
|
+
async function listRead(target, toolId, input) {
|
|
168
|
+
return toRecordArray(await invokeRead(target, toolId, input));
|
|
169
|
+
}
|
|
170
|
+
async function readBoardColumns(target, board) {
|
|
171
|
+
const boardId = normalizeNonEmpty(board.id) ?? normalizeNonEmpty(board.remoteId);
|
|
172
|
+
if (!boardId)
|
|
173
|
+
return [];
|
|
174
|
+
const rows = await listRead(target, 'projectman.kanban-board-column.list', { board: boardId });
|
|
175
|
+
const sorted = [...rows].sort((left, right) => (numberField(left.position) ?? 0) - (numberField(right.position) ?? 0));
|
|
176
|
+
const result = [];
|
|
177
|
+
for (const row of sorted) {
|
|
178
|
+
const boardColumnId = normalizeNonEmpty(row.id) ?? normalizeNonEmpty(row.boardColumnId);
|
|
179
|
+
const columnId = normalizeNonEmpty(row.columnId) ?? normalizeNonEmpty(row.column);
|
|
180
|
+
let column = {};
|
|
181
|
+
if (columnId) {
|
|
182
|
+
try {
|
|
183
|
+
const columnPayload = await invokeRead(target, 'projectman.kanban-column.get', { id: columnId });
|
|
184
|
+
column = isRecord(columnPayload) ? columnPayload : {};
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
column = {};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
result.push(compactPayload({
|
|
191
|
+
...row,
|
|
192
|
+
boardId,
|
|
193
|
+
board: boardId,
|
|
194
|
+
boardColumnId,
|
|
195
|
+
columnId,
|
|
196
|
+
column,
|
|
197
|
+
archiveParentRefs: compactPayload({ boardId, columnId }),
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
function flattenSprintMicrotasks(sprints) {
|
|
203
|
+
const microtasks = [];
|
|
204
|
+
for (const sprint of sprints) {
|
|
205
|
+
const sprintId = normalizeNonEmpty(sprint.id) ?? normalizeNonEmpty(sprint.remoteId);
|
|
206
|
+
const sprintName = normalizeNonEmpty(sprint.name);
|
|
207
|
+
const phases = toRecordArray(sprint.phases);
|
|
208
|
+
for (const phase of phases) {
|
|
209
|
+
const phaseId = normalizeNonEmpty(phase.id);
|
|
210
|
+
const phaseName = normalizeNonEmpty(phase.name);
|
|
211
|
+
for (const microtask of toRecordArray(phase.microtasks)) {
|
|
212
|
+
microtasks.push(compactPayload({
|
|
213
|
+
...microtask,
|
|
214
|
+
sprintId,
|
|
215
|
+
sprintName,
|
|
216
|
+
phaseId,
|
|
217
|
+
phaseName,
|
|
218
|
+
archiveParentRefs: compactPayload({ sprintId, phaseId }),
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const microtask of toRecordArray(sprint.microtasks)) {
|
|
223
|
+
microtasks.push(compactPayload({
|
|
224
|
+
...microtask,
|
|
225
|
+
sprintId,
|
|
226
|
+
sprintName,
|
|
227
|
+
archiveParentRefs: compactPayload({ sprintId }),
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return sortRecords(microtasks);
|
|
232
|
+
}
|
|
233
|
+
async function readPmGraph(target) {
|
|
234
|
+
const scopeId = normalizeNonEmpty(target.resolvedContext.scopeId) ?? normalizeNonEmpty(target.resolvedContext.projectId);
|
|
235
|
+
const projectId = normalizeNonEmpty(target.resolvedContext.projectId);
|
|
236
|
+
if (!scopeId || !projectId)
|
|
237
|
+
throw new Error('Archive target scopeId/projectId could not be resolved.');
|
|
238
|
+
const ownerInput = compactPayload({ scopeId, project: projectId });
|
|
239
|
+
const scopeInput = compactPayload({ scopeId });
|
|
240
|
+
const [boards, tasks, sprints, issues, feedback, reviewRequests] = await Promise.all([
|
|
241
|
+
listRead(target, 'projectman.kanban-board.list', ownerInput),
|
|
242
|
+
listRead(target, 'projectman.kanban-task.list', ownerInput),
|
|
243
|
+
listRead(target, 'projectman.sprint.list', ownerInput),
|
|
244
|
+
listRead(target, 'projectman.issue.list', scopeInput),
|
|
245
|
+
listRead(target, 'projectman.feedback.list', scopeInput),
|
|
246
|
+
listRead(target, 'projectman.review-request.list', scopeInput),
|
|
247
|
+
]);
|
|
248
|
+
const boardColumns = [];
|
|
249
|
+
for (const board of boards) {
|
|
250
|
+
boardColumns.push(...await readBoardColumns(target, board));
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
boards: sortRecords(boards),
|
|
254
|
+
boardColumns: sortRecords(boardColumns),
|
|
255
|
+
tasks: sortRecords(tasks),
|
|
256
|
+
sprints: sortRecords(sprints),
|
|
257
|
+
utasks: flattenSprintMicrotasks(sprints),
|
|
258
|
+
issues: sortRecords(issues),
|
|
259
|
+
feedback: sortRecords(feedback),
|
|
260
|
+
reviewRequests: sortRecords(reviewRequests),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function buildEntityFiles(entitySet) {
|
|
264
|
+
return ARCHIVE_ENTITY_DEFS.map((definition) => {
|
|
265
|
+
const records = sortRecords(entitySet[definition.key] ?? []);
|
|
266
|
+
const checksum = sha256(canonicalJson(records));
|
|
267
|
+
return {
|
|
268
|
+
...definition,
|
|
269
|
+
count: records.length,
|
|
270
|
+
checksum,
|
|
271
|
+
records: records.map((record) => compactPayload({
|
|
272
|
+
remoteId: recordId(record),
|
|
273
|
+
baseHash: sha256(canonicalJson(record)),
|
|
274
|
+
parentRefs: isRecord(record.archiveParentRefs) ? record.archiveParentRefs : undefined,
|
|
275
|
+
})),
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function buildCounts(entityFiles) {
|
|
280
|
+
return entityFiles.reduce((acc, file) => {
|
|
281
|
+
acc[file.key] = file.count;
|
|
282
|
+
return acc;
|
|
283
|
+
}, {});
|
|
284
|
+
}
|
|
285
|
+
function buildBundleChecksum(entityFiles) {
|
|
286
|
+
return sha256(canonicalJson(entityFiles.map((file) => ({
|
|
287
|
+
key: file.key,
|
|
288
|
+
entityType: file.entityType,
|
|
289
|
+
count: file.count,
|
|
290
|
+
checksum: file.checksum,
|
|
291
|
+
}))));
|
|
292
|
+
}
|
|
293
|
+
function buildManifest(params) {
|
|
294
|
+
const projectId = normalizeNonEmpty(params.target.resolvedContext.projectId);
|
|
295
|
+
const scopeId = normalizeNonEmpty(params.target.resolvedContext.scopeId) ?? projectId;
|
|
296
|
+
const projectSlug = normalizeNonEmpty(params.target.resolvedContext.projectSlug) ?? normalizeNonEmpty(params.target.project.slug);
|
|
297
|
+
const entityFiles = params.entityFiles.map(({ records: _records, ...file }) => file);
|
|
298
|
+
const bundleChecksum = buildBundleChecksum(params.entityFiles);
|
|
299
|
+
return {
|
|
300
|
+
schemaVersion: 2,
|
|
301
|
+
entityType: ARCHIVE_ENTITY_TYPE,
|
|
302
|
+
archiveKind: ARCHIVE_KIND,
|
|
303
|
+
archiveVersion: 1,
|
|
304
|
+
partial: true,
|
|
305
|
+
decommissionSafe: false,
|
|
306
|
+
pendingDomains: DEFAULT_PENDING_DOMAINS,
|
|
307
|
+
createdAt: params.createdAt,
|
|
308
|
+
project: compactPayload({
|
|
309
|
+
id: projectId,
|
|
310
|
+
projectId,
|
|
311
|
+
scopeId,
|
|
312
|
+
slug: projectSlug,
|
|
313
|
+
name: normalizeNonEmpty(params.target.resolvedContext.projectName) ?? normalizeNonEmpty(params.target.project.name),
|
|
314
|
+
status: normalizeNonEmpty(params.target.project.status),
|
|
315
|
+
}),
|
|
316
|
+
source: compactPayload({
|
|
317
|
+
command: 'archive.create',
|
|
318
|
+
archiveDir: path.relative(params.target.resolvedContext.repoRoot, params.archiveDir).split(path.sep).join('/'),
|
|
319
|
+
apiBaseUrl: normalizeNonEmpty(String(params.target.apiState.baseUrl ?? '')),
|
|
320
|
+
}),
|
|
321
|
+
counts: buildCounts(params.entityFiles),
|
|
322
|
+
files: entityFiles,
|
|
323
|
+
checksums: {
|
|
324
|
+
bundle: bundleChecksum,
|
|
325
|
+
entities: entityFiles.reduce((acc, file) => {
|
|
326
|
+
acc[file.key] = file.checksum;
|
|
327
|
+
return acc;
|
|
328
|
+
}, {}),
|
|
329
|
+
},
|
|
330
|
+
verification: {
|
|
331
|
+
status: 'pending',
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
async function writeArchiveBundle(params) {
|
|
336
|
+
const written = [];
|
|
337
|
+
for (const definition of ARCHIVE_ENTITY_DEFS) {
|
|
338
|
+
const filePath = path.join(params.archiveDir, definition.file);
|
|
339
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
340
|
+
await writeFileWithRetry(filePath, canonicalJson(sortRecords(params.entitySet[definition.key] ?? [])), 'utf8');
|
|
341
|
+
written.push(filePath);
|
|
342
|
+
}
|
|
343
|
+
const manifestPath = path.join(params.archiveDir, 'manifest.json');
|
|
344
|
+
await writeFileWithRetry(manifestPath, canonicalJson(params.manifest), 'utf8');
|
|
345
|
+
written.push(manifestPath);
|
|
346
|
+
return written;
|
|
347
|
+
}
|
|
348
|
+
function emit(options, envelope, message) {
|
|
349
|
+
if (options.json) {
|
|
350
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
logSuccess(message);
|
|
354
|
+
console.log(JSON.stringify(envelope.result ?? envelope, null, 2));
|
|
355
|
+
}
|
|
356
|
+
function assertArchiveManifest(manifest) {
|
|
357
|
+
if (normalizeNonEmpty(manifest.entityType) !== ARCHIVE_ENTITY_TYPE) {
|
|
358
|
+
throw new Error(`Manifest is not an AOPS PM archive bundle: expected entityType ${ARCHIVE_ENTITY_TYPE}.`);
|
|
359
|
+
}
|
|
360
|
+
if (normalizeNonEmpty(manifest.archiveKind) !== ARCHIVE_KIND) {
|
|
361
|
+
throw new Error(`Unsupported archiveKind "${normalizeNonEmpty(manifest.archiveKind) ?? '(missing)'}".`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function readArchiveManifest(manifestPath) {
|
|
365
|
+
const raw = await fs.readFile(manifestPath, 'utf8');
|
|
366
|
+
const manifest = JSON.parse(raw);
|
|
367
|
+
if (!isRecord(manifest))
|
|
368
|
+
throw new Error('Archive manifest root must be an object.');
|
|
369
|
+
assertArchiveManifest(manifest);
|
|
370
|
+
return manifest;
|
|
371
|
+
}
|
|
372
|
+
async function verifyLocalBundleFiles(manifestPath, manifest) {
|
|
373
|
+
const manifestDir = path.dirname(manifestPath);
|
|
374
|
+
const files = toRecordArray(manifest.files);
|
|
375
|
+
if (files.length === 0)
|
|
376
|
+
throw new Error('Archive manifest has no entity files.');
|
|
377
|
+
const verified = [];
|
|
378
|
+
for (const file of files) {
|
|
379
|
+
const key = normalizeNonEmpty(file.key);
|
|
380
|
+
const entityType = normalizeNonEmpty(file.entityType);
|
|
381
|
+
const relativeFile = normalizeNonEmpty(file.file);
|
|
382
|
+
const expectedChecksum = normalizeNonEmpty(file.checksum);
|
|
383
|
+
if (!key || !entityType || !relativeFile || !expectedChecksum) {
|
|
384
|
+
throw new Error('Archive manifest contains an incomplete file entry.');
|
|
385
|
+
}
|
|
386
|
+
const filePath = path.join(manifestDir, relativeFile);
|
|
387
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
388
|
+
const actualChecksum = sha256(raw);
|
|
389
|
+
if (actualChecksum !== expectedChecksum) {
|
|
390
|
+
throw new Error(`Archive entity file checksum mismatch for ${relativeFile}.`);
|
|
391
|
+
}
|
|
392
|
+
const parsed = JSON.parse(raw);
|
|
393
|
+
const records = toRecordArray(parsed);
|
|
394
|
+
verified.push({
|
|
395
|
+
key,
|
|
396
|
+
entityType,
|
|
397
|
+
file: relativeFile,
|
|
398
|
+
count: records.length,
|
|
399
|
+
checksum: actualChecksum,
|
|
400
|
+
records: records.map((record) => compactPayload({
|
|
401
|
+
...record,
|
|
402
|
+
remoteId: recordId(record),
|
|
403
|
+
baseHash: sha256(canonicalJson(record)),
|
|
404
|
+
parentRefs: isRecord(record.archiveParentRefs) ? record.archiveParentRefs : undefined,
|
|
405
|
+
})),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return verified;
|
|
409
|
+
}
|
|
410
|
+
function projectFromManifest(manifest) {
|
|
411
|
+
const project = isRecord(manifest.project) ? manifest.project : {};
|
|
412
|
+
return {
|
|
413
|
+
projectSlug: normalizeNonEmpty(project.slug),
|
|
414
|
+
projectId: normalizeNonEmpty(project.projectId) ?? normalizeNonEmpty(project.id),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function assertVerificationMatches(localFiles, remoteFiles) {
|
|
418
|
+
const localByKey = new Map(localFiles.map((file) => [file.key, file]));
|
|
419
|
+
for (const remoteFile of remoteFiles) {
|
|
420
|
+
const localFile = localByKey.get(remoteFile.key);
|
|
421
|
+
if (!localFile)
|
|
422
|
+
throw new Error(`Archive verification missing local file for ${remoteFile.key}.`);
|
|
423
|
+
if (localFile.count !== remoteFile.count) {
|
|
424
|
+
throw new Error(`Archive verification count mismatch for ${remoteFile.key}: local=${localFile.count}, hosted=${remoteFile.count}.`);
|
|
425
|
+
}
|
|
426
|
+
if (localFile.checksum !== remoteFile.checksum) {
|
|
427
|
+
throw new Error(`Archive verification checksum mismatch for ${remoteFile.key}.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function assertManifestVerified(manifest) {
|
|
432
|
+
const verification = isRecord(manifest.verification) ? manifest.verification : {};
|
|
433
|
+
if (normalizeNonEmpty(verification.status) !== 'passed') {
|
|
434
|
+
throw new Error('Archive manifest verification must be passed before delete. Run archive verify --manifest <path> --apply first.');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function actionKey(action) {
|
|
438
|
+
const sprintId = normalizeNonEmpty(action.input.id);
|
|
439
|
+
const microTask = normalizeNonEmpty(action.input.microTask);
|
|
440
|
+
if (action.toolId === 'projectman.sprint.delete-microtask' && sprintId && microTask) {
|
|
441
|
+
return `${action.toolId}:${sprintId}:${microTask}`;
|
|
442
|
+
}
|
|
443
|
+
return `${action.toolId}:${action.id}`;
|
|
444
|
+
}
|
|
445
|
+
function archivedDeletionActions(manifest) {
|
|
446
|
+
const deletion = isRecord(manifest.deletion) ? manifest.deletion : {};
|
|
447
|
+
const actions = toRecordArray(deletion.actions);
|
|
448
|
+
const entries = [];
|
|
449
|
+
for (const action of actions) {
|
|
450
|
+
const key = normalizeNonEmpty(action.key);
|
|
451
|
+
if (key)
|
|
452
|
+
entries.push([key, action]);
|
|
453
|
+
}
|
|
454
|
+
return new Map(entries);
|
|
455
|
+
}
|
|
456
|
+
function mergeDeletionActions(plannedActions, previous, overrides = new Map()) {
|
|
457
|
+
return plannedActions.map((action, index) => {
|
|
458
|
+
const key = actionKey(action);
|
|
459
|
+
const previousAction = previous.get(key) ?? {};
|
|
460
|
+
const override = overrides.get(key) ?? {};
|
|
461
|
+
return compactPayload({
|
|
462
|
+
key,
|
|
463
|
+
position: index,
|
|
464
|
+
entityType: action.entityType,
|
|
465
|
+
toolId: action.toolId,
|
|
466
|
+
id: action.id,
|
|
467
|
+
input: action.input,
|
|
468
|
+
sourceFile: action.sourceFile,
|
|
469
|
+
status: normalizeNonEmpty(override.status) ?? normalizeNonEmpty(previousAction.status) ?? 'pending',
|
|
470
|
+
deletedAt: normalizeNonEmpty(override.deletedAt) ?? normalizeNonEmpty(previousAction.deletedAt),
|
|
471
|
+
missingAt: normalizeNonEmpty(override.missingAt) ?? normalizeNonEmpty(previousAction.missingAt),
|
|
472
|
+
error: normalizeNonEmpty(override.error) ?? normalizeNonEmpty(previousAction.error),
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function readDeletionStatus(manifest, action) {
|
|
477
|
+
const previous = archivedDeletionActions(manifest).get(actionKey(action));
|
|
478
|
+
return normalizeNonEmpty(previous?.status);
|
|
479
|
+
}
|
|
480
|
+
function buildDeletePlanFromFiles(localFiles) {
|
|
481
|
+
const filesByKey = new Map(localFiles.map((file) => [file.key, file]));
|
|
482
|
+
const actions = [];
|
|
483
|
+
const seenColumns = new Set();
|
|
484
|
+
for (const order of DELETE_ORDER) {
|
|
485
|
+
if (order.key === 'columns') {
|
|
486
|
+
const boardColumns = filesByKey.get('boardColumns');
|
|
487
|
+
for (const record of boardColumns?.records ?? []) {
|
|
488
|
+
const parentRefs = isRecord(record.parentRefs) ? record.parentRefs : {};
|
|
489
|
+
const columnId = normalizeNonEmpty(record.columnId)
|
|
490
|
+
?? normalizeNonEmpty(record.column)
|
|
491
|
+
?? normalizeNonEmpty(parentRefs.columnId);
|
|
492
|
+
if (!columnId || seenColumns.has(columnId))
|
|
493
|
+
continue;
|
|
494
|
+
seenColumns.add(columnId);
|
|
495
|
+
actions.push({
|
|
496
|
+
key: 'columns',
|
|
497
|
+
entityType: 'projectman.kanban-column',
|
|
498
|
+
toolId: order.toolId,
|
|
499
|
+
id: columnId,
|
|
500
|
+
input: { id: columnId },
|
|
501
|
+
sourceFile: 'entities/board-columns.json',
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const file = filesByKey.get(order.key);
|
|
507
|
+
if (!file)
|
|
508
|
+
continue;
|
|
509
|
+
for (const record of file.records) {
|
|
510
|
+
const id = normalizeNonEmpty(record.remoteId);
|
|
511
|
+
if (!id) {
|
|
512
|
+
throw new Error(`Archive ${file.file} contains a record without remoteId; refusing to build delete plan.`);
|
|
513
|
+
}
|
|
514
|
+
if (order.toolId === 'projectman.sprint.delete-microtask') {
|
|
515
|
+
const parentRefs = isRecord(record.parentRefs) ? record.parentRefs : {};
|
|
516
|
+
const sprintId = normalizeNonEmpty(parentRefs.sprintId) ?? normalizeNonEmpty(record.sprintId);
|
|
517
|
+
if (!sprintId) {
|
|
518
|
+
throw new Error(`Archive ${file.file} microtask ${id} is missing sprintId parent reference.`);
|
|
519
|
+
}
|
|
520
|
+
actions.push({
|
|
521
|
+
key: file.key,
|
|
522
|
+
entityType: file.entityType,
|
|
523
|
+
toolId: order.toolId,
|
|
524
|
+
id,
|
|
525
|
+
input: { id: sprintId, microTask: id },
|
|
526
|
+
sourceFile: file.file,
|
|
527
|
+
});
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
actions.push({
|
|
531
|
+
key: file.key,
|
|
532
|
+
entityType: file.entityType,
|
|
533
|
+
toolId: order.toolId,
|
|
534
|
+
id,
|
|
535
|
+
input: { id },
|
|
536
|
+
sourceFile: file.file,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return actions;
|
|
541
|
+
}
|
|
542
|
+
function isMissingDeleteError(error) {
|
|
543
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
544
|
+
return /(^|[^0-9])404([^0-9]|$)|not[_ -]?found|record not found|missing/i.test(message);
|
|
545
|
+
}
|
|
546
|
+
async function invokeDelete(target, action) {
|
|
547
|
+
await invokeHostedToolWithApiState(target.apiState, {
|
|
548
|
+
...target.gatewayOptions,
|
|
549
|
+
toolId: action.toolId,
|
|
550
|
+
input: action.input,
|
|
551
|
+
preview: false,
|
|
552
|
+
apply: true,
|
|
553
|
+
confirm: true,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
function buildDeletionManifest(manifest, plannedActions, params) {
|
|
557
|
+
const previousDeletion = isRecord(manifest.deletion) ? manifest.deletion : {};
|
|
558
|
+
const previousActions = archivedDeletionActions(manifest);
|
|
559
|
+
const actions = mergeDeletionActions(plannedActions, previousActions, params.overrides);
|
|
560
|
+
const completed = actions.filter((action) => ['deleted', 'missing'].includes(String(action.status))).length;
|
|
561
|
+
return {
|
|
562
|
+
...manifest,
|
|
563
|
+
deletion: compactPayload({
|
|
564
|
+
status: params.status,
|
|
565
|
+
mode: 'hosted-pm-delete',
|
|
566
|
+
startedAt: params.startedAt ?? normalizeNonEmpty(previousDeletion.startedAt),
|
|
567
|
+
completedAt: params.completedAt,
|
|
568
|
+
failedAt: params.failedAt,
|
|
569
|
+
lastError: params.lastError,
|
|
570
|
+
total: plannedActions.length,
|
|
571
|
+
completed,
|
|
572
|
+
pending: plannedActions.length - completed,
|
|
573
|
+
order: DELETE_ORDER.map((entry) => entry.key),
|
|
574
|
+
actions,
|
|
575
|
+
}),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function pendingDomainsFromManifest(manifest) {
|
|
579
|
+
const pendingDomains = manifest.pendingDomains;
|
|
580
|
+
return Array.isArray(pendingDomains)
|
|
581
|
+
? pendingDomains.map((entry) => normalizeNonEmpty(entry)).filter((entry) => Boolean(entry))
|
|
582
|
+
: [];
|
|
583
|
+
}
|
|
584
|
+
function decommissionSafeFromManifest(manifest) {
|
|
585
|
+
return manifest.decommissionSafe === true;
|
|
586
|
+
}
|
|
587
|
+
export async function runArchiveCreate(options) {
|
|
588
|
+
try {
|
|
589
|
+
if (options.apply !== true)
|
|
590
|
+
throw new Error('archive create writes a local bundle. Retry with --apply.');
|
|
591
|
+
const target = await resolveArchiveTarget(options);
|
|
592
|
+
if (!target)
|
|
593
|
+
return;
|
|
594
|
+
const projectSlug = normalizeNonEmpty(target.resolvedContext.projectSlug) ?? normalizeNonEmpty(target.project.slug) ?? normalizeNonEmpty(target.resolvedContext.projectId) ?? 'project';
|
|
595
|
+
const archiveDir = path.join(archiveRoot(target.resolvedContext.repoRoot, options.outputRoot), projectSlug, safeTimestamp(options.timestamp));
|
|
596
|
+
const createdAt = new Date().toISOString();
|
|
597
|
+
const entitySet = await readPmGraph(target);
|
|
598
|
+
const entityFiles = buildEntityFiles(entitySet);
|
|
599
|
+
const manifest = buildManifest({ target, entityFiles, archiveDir, createdAt });
|
|
600
|
+
const filesWritten = (await writeArchiveBundle({ archiveDir, manifest, entitySet }))
|
|
601
|
+
.map((filePath) => path.relative(target.resolvedContext.repoRoot, filePath).split(path.sep).join('/'));
|
|
602
|
+
emit(options, {
|
|
603
|
+
command: 'archive.create',
|
|
604
|
+
surface: 'aops-archive-v1',
|
|
605
|
+
resolvedContext: target.resolvedContext,
|
|
606
|
+
result: {
|
|
607
|
+
ok: true,
|
|
608
|
+
data: {
|
|
609
|
+
archiveDir: path.relative(target.resolvedContext.repoRoot, archiveDir).split(path.sep).join('/'),
|
|
610
|
+
manifestPath: path.relative(target.resolvedContext.repoRoot, path.join(archiveDir, 'manifest.json')).split(path.sep).join('/'),
|
|
611
|
+
counts: manifest.counts,
|
|
612
|
+
checksums: manifest.checksums,
|
|
613
|
+
pendingDomains: DEFAULT_PENDING_DOMAINS,
|
|
614
|
+
decommissionSafe: false,
|
|
615
|
+
filesWritten,
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
}, 'Archive bundle created.');
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
622
|
+
process.exitCode = 1;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
export async function runArchiveVerify(options) {
|
|
626
|
+
try {
|
|
627
|
+
const manifestInput = normalizeNonEmpty(options.manifest);
|
|
628
|
+
if (!manifestInput)
|
|
629
|
+
throw new Error('Missing required --manifest.');
|
|
630
|
+
const repoRoot = (await resolveProjectBindingContext(options, { requireProject: false })).repoRoot;
|
|
631
|
+
const manifestPath = resolveRepoRelativePath(repoRoot, manifestInput);
|
|
632
|
+
const manifest = await readArchiveManifest(manifestPath);
|
|
633
|
+
const manifestProject = projectFromManifest(manifest);
|
|
634
|
+
if (options.projectSlug && manifestProject.projectSlug && options.projectSlug !== manifestProject.projectSlug) {
|
|
635
|
+
throw new Error(`--project-slug ${options.projectSlug} does not match manifest project slug ${manifestProject.projectSlug}.`);
|
|
636
|
+
}
|
|
637
|
+
const target = await resolveArchiveTarget(options, manifestProject);
|
|
638
|
+
if (!target)
|
|
639
|
+
return;
|
|
640
|
+
const localFiles = await verifyLocalBundleFiles(manifestPath, manifest);
|
|
641
|
+
const remoteEntitySet = await readPmGraph(target);
|
|
642
|
+
const remoteFiles = buildEntityFiles(remoteEntitySet);
|
|
643
|
+
assertVerificationMatches(localFiles, remoteFiles);
|
|
644
|
+
const remoteBundleChecksum = buildBundleChecksum(remoteFiles);
|
|
645
|
+
const manifestChecksums = isRecord(manifest.checksums) ? manifest.checksums : {};
|
|
646
|
+
const localBundleChecksum = normalizeNonEmpty(manifestChecksums.bundle);
|
|
647
|
+
if (localBundleChecksum && localBundleChecksum !== remoteBundleChecksum) {
|
|
648
|
+
throw new Error('Archive verification bundle checksum mismatch.');
|
|
649
|
+
}
|
|
650
|
+
const verification = {
|
|
651
|
+
status: 'passed',
|
|
652
|
+
verifiedAt: new Date().toISOString(),
|
|
653
|
+
mode: 'hosted-refetch',
|
|
654
|
+
counts: buildCounts(remoteFiles),
|
|
655
|
+
checksum: remoteBundleChecksum,
|
|
656
|
+
};
|
|
657
|
+
const nextManifest = {
|
|
658
|
+
...manifest,
|
|
659
|
+
verification,
|
|
660
|
+
verifiedAt: verification.verifiedAt,
|
|
661
|
+
};
|
|
662
|
+
if (options.apply === true) {
|
|
663
|
+
await writeFileWithRetry(manifestPath, canonicalJson(nextManifest), 'utf8');
|
|
664
|
+
}
|
|
665
|
+
emit(options, {
|
|
666
|
+
command: 'archive.verify',
|
|
667
|
+
surface: 'aops-archive-v1',
|
|
668
|
+
resolvedContext: target.resolvedContext,
|
|
669
|
+
result: {
|
|
670
|
+
ok: true,
|
|
671
|
+
data: {
|
|
672
|
+
manifestPath: path.relative(target.resolvedContext.repoRoot, manifestPath).split(path.sep).join('/'),
|
|
673
|
+
verification,
|
|
674
|
+
wroteManifest: options.apply === true,
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
}, options.apply === true ? 'Archive bundle verified.' : 'Archive bundle verification passed; rerun with --apply to persist verification.');
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
681
|
+
process.exitCode = 1;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
export async function runArchiveDelete(options) {
|
|
685
|
+
try {
|
|
686
|
+
const manifestInput = normalizeNonEmpty(options.manifest);
|
|
687
|
+
if (!manifestInput)
|
|
688
|
+
throw new Error('Missing required --manifest.');
|
|
689
|
+
const repoRoot = (await resolveProjectBindingContext(options, { requireProject: false })).repoRoot;
|
|
690
|
+
const manifestPath = resolveRepoRelativePath(repoRoot, manifestInput);
|
|
691
|
+
const manifest = await readArchiveManifest(manifestPath);
|
|
692
|
+
assertManifestVerified(manifest);
|
|
693
|
+
const target = await resolveArchiveTarget(options, projectFromManifest(manifest));
|
|
694
|
+
if (!target)
|
|
695
|
+
return;
|
|
696
|
+
const localFiles = await verifyLocalBundleFiles(manifestPath, manifest);
|
|
697
|
+
const plannedActions = buildDeletePlanFromFiles(localFiles);
|
|
698
|
+
const previousActions = archivedDeletionActions(manifest);
|
|
699
|
+
const runnableActions = plannedActions.filter((action) => {
|
|
700
|
+
const status = readDeletionStatus(manifest, action);
|
|
701
|
+
return status !== 'deleted' && status !== 'missing';
|
|
702
|
+
});
|
|
703
|
+
if (options.apply !== true) {
|
|
704
|
+
emit(options, {
|
|
705
|
+
command: 'archive.delete',
|
|
706
|
+
surface: 'aops-archive-v1',
|
|
707
|
+
resolvedContext: target.resolvedContext,
|
|
708
|
+
result: {
|
|
709
|
+
ok: true,
|
|
710
|
+
data: {
|
|
711
|
+
mode: 'preview',
|
|
712
|
+
manifestPath: path.relative(target.resolvedContext.repoRoot, manifestPath).split(path.sep).join('/'),
|
|
713
|
+
actionCount: plannedActions.length,
|
|
714
|
+
runnableActionCount: runnableActions.length,
|
|
715
|
+
order: DELETE_ORDER.map((entry) => entry.key),
|
|
716
|
+
actions: mergeDeletionActions(plannedActions, previousActions),
|
|
717
|
+
requires: ['--apply', '--confirm'],
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
}, 'Archive delete preview ready.');
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (options.confirm !== true)
|
|
724
|
+
throw new Error('archive delete is destructive. Retry with --apply --confirm or omit --apply for preview.');
|
|
725
|
+
const startedAt = new Date().toISOString();
|
|
726
|
+
let currentManifest = buildDeletionManifest(manifest, plannedActions, {
|
|
727
|
+
status: 'in_progress',
|
|
728
|
+
startedAt,
|
|
729
|
+
});
|
|
730
|
+
await writeFileWithRetry(manifestPath, canonicalJson(currentManifest), 'utf8');
|
|
731
|
+
const overrides = new Map();
|
|
732
|
+
for (const action of plannedActions) {
|
|
733
|
+
const existingStatus = readDeletionStatus(currentManifest, action);
|
|
734
|
+
if (existingStatus === 'deleted' || existingStatus === 'missing')
|
|
735
|
+
continue;
|
|
736
|
+
const key = actionKey(action);
|
|
737
|
+
try {
|
|
738
|
+
await invokeDelete(target, action);
|
|
739
|
+
overrides.set(key, { status: 'deleted', deletedAt: new Date().toISOString() });
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
if (isMissingDeleteError(error)) {
|
|
743
|
+
overrides.set(key, { status: 'missing', missingAt: new Date().toISOString() });
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
747
|
+
overrides.set(key, { status: 'failed', error: message });
|
|
748
|
+
currentManifest = buildDeletionManifest(currentManifest, plannedActions, {
|
|
749
|
+
status: 'failed',
|
|
750
|
+
startedAt,
|
|
751
|
+
failedAt: new Date().toISOString(),
|
|
752
|
+
lastError: message,
|
|
753
|
+
overrides,
|
|
754
|
+
});
|
|
755
|
+
await writeFileWithRetry(manifestPath, canonicalJson(currentManifest), 'utf8');
|
|
756
|
+
throw new Error(`Archive delete failed at ${action.toolId} ${action.id}; manifest recorded failure and delete stopped. ${message}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
currentManifest = buildDeletionManifest(currentManifest, plannedActions, {
|
|
760
|
+
status: 'in_progress',
|
|
761
|
+
startedAt,
|
|
762
|
+
overrides,
|
|
763
|
+
});
|
|
764
|
+
await writeFileWithRetry(manifestPath, canonicalJson(currentManifest), 'utf8');
|
|
765
|
+
}
|
|
766
|
+
const completedAt = new Date().toISOString();
|
|
767
|
+
currentManifest = buildDeletionManifest(currentManifest, plannedActions, {
|
|
768
|
+
status: 'completed',
|
|
769
|
+
startedAt,
|
|
770
|
+
completedAt,
|
|
771
|
+
overrides,
|
|
772
|
+
});
|
|
773
|
+
await writeFileWithRetry(manifestPath, canonicalJson(currentManifest), 'utf8');
|
|
774
|
+
emit(options, {
|
|
775
|
+
command: 'archive.delete',
|
|
776
|
+
surface: 'aops-archive-v1',
|
|
777
|
+
resolvedContext: target.resolvedContext,
|
|
778
|
+
result: {
|
|
779
|
+
ok: true,
|
|
780
|
+
data: {
|
|
781
|
+
mode: 'apply',
|
|
782
|
+
manifestPath: path.relative(target.resolvedContext.repoRoot, manifestPath).split(path.sep).join('/'),
|
|
783
|
+
actionCount: plannedActions.length,
|
|
784
|
+
executedActionCount: runnableActions.length,
|
|
785
|
+
deletion: currentManifest.deletion,
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
}, 'Archive PM graph delete completed.');
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
792
|
+
process.exitCode = 1;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
export async function runArchiveDecommissionCheck(options) {
|
|
796
|
+
try {
|
|
797
|
+
const manifestInput = normalizeNonEmpty(options.manifest);
|
|
798
|
+
if (!manifestInput)
|
|
799
|
+
throw new Error('Missing required --manifest.');
|
|
800
|
+
const repoRoot = (await resolveProjectBindingContext(options, { requireProject: false })).repoRoot;
|
|
801
|
+
const manifestPath = resolveRepoRelativePath(repoRoot, manifestInput);
|
|
802
|
+
const manifest = await readArchiveManifest(manifestPath);
|
|
803
|
+
const pendingDomains = pendingDomainsFromManifest(manifest);
|
|
804
|
+
const decommissionSafe = decommissionSafeFromManifest(manifest);
|
|
805
|
+
const blocked = pendingDomains.length > 0 || decommissionSafe !== true;
|
|
806
|
+
const data = {
|
|
807
|
+
manifestPath: path.relative(repoRoot, manifestPath).split(path.sep).join('/'),
|
|
808
|
+
status: blocked ? 'blocked' : 'allowed',
|
|
809
|
+
decommissionSafe,
|
|
810
|
+
pendingDomains,
|
|
811
|
+
message: blocked
|
|
812
|
+
? `Full project/scope decommission is blocked because pendingDomains is non-empty or decommissionSafe is not true: ${pendingDomains.join(', ') || '(none)'}`
|
|
813
|
+
: 'Full project/scope decommission is allowed by this archive manifest.',
|
|
814
|
+
};
|
|
815
|
+
emit(options, {
|
|
816
|
+
command: 'archive.decommission-check',
|
|
817
|
+
surface: 'aops-archive-v1',
|
|
818
|
+
result: {
|
|
819
|
+
ok: !blocked,
|
|
820
|
+
data,
|
|
821
|
+
},
|
|
822
|
+
}, blocked ? 'Archive decommission check blocked.' : 'Archive decommission check passed.');
|
|
823
|
+
if (blocked)
|
|
824
|
+
process.exitCode = 1;
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
828
|
+
process.exitCode = 1;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
export function makeArchiveCommand() {
|
|
832
|
+
const cmd = new Command('archive')
|
|
833
|
+
.description('AOPS archive bundle commands for hosted Projectman graph cleanup preparation');
|
|
834
|
+
applyCommonOptions(cmd.command('create')
|
|
835
|
+
.description('Download a hosted PM graph into .aops/archive/<slug>/<timestamp>')
|
|
836
|
+
.requiredOption('--project-slug <slug>', 'Project slug from the repo project registry')
|
|
837
|
+
.option('--output-root <path>', 'Archive root directory', '.aops/archive')
|
|
838
|
+
.option('--timestamp <value>', 'Archive timestamp/path segment override')
|
|
839
|
+
.option('--apply', 'Write the local archive bundle')
|
|
840
|
+
.action(async (options) => runArchiveCreate(options)), { withProject: true });
|
|
841
|
+
applyCommonOptions(cmd.command('verify')
|
|
842
|
+
.description('Verify an archive bundle by re-fetching hosted PM graph data and comparing counts/checksums')
|
|
843
|
+
.requiredOption('--manifest <path>', 'Path to archive manifest.json')
|
|
844
|
+
.option('--project-slug <slug>', 'Optional project slug assertion')
|
|
845
|
+
.option('--apply', 'Persist verification:passed into the manifest')
|
|
846
|
+
.action(async (options) => runArchiveVerify(options)), { withProject: true });
|
|
847
|
+
applyCommonOptions(cmd.command('delete')
|
|
848
|
+
.description('Preview or apply destructive hosted PM graph deletion from a verified archive manifest')
|
|
849
|
+
.requiredOption('--manifest <path>', 'Path to archive manifest.json')
|
|
850
|
+
.option('--preview', 'Preview delete plan without mutating hosted state')
|
|
851
|
+
.option('--apply', 'Execute destructive hosted deletes')
|
|
852
|
+
.option('--confirm', 'Required with --apply for destructive deletes')
|
|
853
|
+
.action(async (options) => runArchiveDelete(options)), { withProject: true });
|
|
854
|
+
applyCommonOptions(cmd.command('decommission-check')
|
|
855
|
+
.description('Check whether an archive manifest permits full project/scope decommission')
|
|
856
|
+
.requiredOption('--manifest <path>', 'Path to archive manifest.json')
|
|
857
|
+
.action(async (options) => runArchiveDecommissionCheck(options)), { withProject: true });
|
|
858
|
+
cmd.addHelpText('after', `
|
|
859
|
+
Examples:
|
|
860
|
+
aops-cli archive create --project-slug aops --apply --json
|
|
861
|
+
aops-cli archive verify --manifest .aops/archive/aops/<ts>/manifest.json --apply --json
|
|
862
|
+
aops-cli archive delete --manifest .aops/archive/aops/<ts>/manifest.json --json
|
|
863
|
+
aops-cli archive delete --manifest .aops/archive/aops/<ts>/manifest.json --apply --confirm --json
|
|
864
|
+
aops-cli archive decommission-check --manifest .aops/archive/aops/<ts>/manifest.json --json
|
|
865
|
+
|
|
866
|
+
Notes:
|
|
867
|
+
create/verify never delete hosted records.
|
|
868
|
+
delete is destructive only with --apply --confirm; without --apply it is a preview.
|
|
869
|
+
delete requires manifest verification:passed and records per-action deletion state for resumability.
|
|
870
|
+
Archive bundles live under .aops/archive/** and are not repo-first sync sources.
|
|
871
|
+
`);
|
|
872
|
+
return cmd;
|
|
873
|
+
}
|