@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,1080 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { logError, logSuccess } from '@aopslabs/xf-cli-ui';
|
|
7
|
+
import { applyCommonOptions, compactPayload, normalizeNonEmpty } from '../utils/command.js';
|
|
8
|
+
import { invokeHostedToolWithApiState, parseJsonInput, requireApiState, unwrapHostedToolResult, } from '../utils/agent-gateway.js';
|
|
9
|
+
import { preferProjectNameBinding, resolveOwnerScopeIdFromBinding, resolveOwnerScopeIdFromProjectRecord, resolveProjectBindingContext, } from '../utils/project-context.js';
|
|
10
|
+
import { loadAopsRepoConfig, writeAopsRepoConfig, } from '../utils/repo-config.js';
|
|
11
|
+
import { buildHostedSugarEnvelope, buildOperatorCookbook, ensureDestructiveWrite, ensureGuardedWrite, } from '../utils/hosted-sugar.js';
|
|
12
|
+
import { GUIDE_PATHS } from '../utils/guide-paths.js';
|
|
13
|
+
import { renderTable } from '../utils/table.js';
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function unwrapResultData(result) {
|
|
18
|
+
if (isRecord(result) && Object.prototype.hasOwnProperty.call(result, 'data')) {
|
|
19
|
+
return result.data;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
function unwrapListItems(result) {
|
|
24
|
+
const data = unwrapResultData(result);
|
|
25
|
+
if (Array.isArray(data)) {
|
|
26
|
+
return data.filter((entry) => isRecord(entry));
|
|
27
|
+
}
|
|
28
|
+
if (isRecord(data) && Array.isArray(data.items)) {
|
|
29
|
+
return data.items.filter((entry) => isRecord(entry));
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(result)) {
|
|
32
|
+
return result.filter((entry) => isRecord(entry));
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
function parseJsonSeed(input, label = '--input') {
|
|
37
|
+
const normalized = normalizeNonEmpty(input);
|
|
38
|
+
if (!normalized)
|
|
39
|
+
return {};
|
|
40
|
+
const parsed = parseJsonInput(normalized, label);
|
|
41
|
+
if (!isRecord(parsed))
|
|
42
|
+
throw new Error(`${label} must be a JSON object or @file.json object.`);
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
function resolveStringField(explicit, seed, key) {
|
|
46
|
+
return normalizeNonEmpty(explicit) ?? normalizeNonEmpty(seed[key]);
|
|
47
|
+
}
|
|
48
|
+
function toInteger(value, label) {
|
|
49
|
+
if (typeof value === 'number' && Number.isInteger(value))
|
|
50
|
+
return value;
|
|
51
|
+
const normalized = normalizeNonEmpty(value);
|
|
52
|
+
if (!normalized)
|
|
53
|
+
throw new Error(`${label} must be an integer.`);
|
|
54
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
55
|
+
if (!Number.isInteger(parsed))
|
|
56
|
+
throw new Error(`${label} must be an integer.`);
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
function buildGatewayOptions(options, resolvedContext) {
|
|
60
|
+
return {
|
|
61
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
62
|
+
accessToken: options.accessToken,
|
|
63
|
+
refreshToken: options.refreshToken,
|
|
64
|
+
timeoutMs: options.timeoutMs,
|
|
65
|
+
tenantId: options.tenantId,
|
|
66
|
+
locale: options.locale,
|
|
67
|
+
fallbackLocale: options.fallbackLocale,
|
|
68
|
+
scopeId: options.scopeId,
|
|
69
|
+
projectId: options.projectId,
|
|
70
|
+
projectName: options.projectName,
|
|
71
|
+
scopeResolution: options.scopeResolution,
|
|
72
|
+
...preferProjectNameBinding(resolvedContext),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function hydrateProjectContext(apiState, options, resolvedContext) {
|
|
76
|
+
const projectId = normalizeNonEmpty(resolvedContext.projectId);
|
|
77
|
+
if (!projectId)
|
|
78
|
+
return resolvedContext;
|
|
79
|
+
if (normalizeNonEmpty(options.scopeId)) {
|
|
80
|
+
return resolvedContext;
|
|
81
|
+
}
|
|
82
|
+
const payload = await invokeHostedToolWithApiState(apiState, {
|
|
83
|
+
...buildGatewayOptions(options, resolvedContext),
|
|
84
|
+
toolId: 'agentspace.project.get-by-id',
|
|
85
|
+
input: { id: projectId },
|
|
86
|
+
});
|
|
87
|
+
const result = unwrapHostedToolResult(payload);
|
|
88
|
+
const project = unwrapResultData(result);
|
|
89
|
+
if (!isRecord(project))
|
|
90
|
+
return resolvedContext;
|
|
91
|
+
const scopeId = resolveOwnerScopeIdFromProjectRecord(project, resolvedContext.scopeId);
|
|
92
|
+
const projectName = normalizeNonEmpty(project.name) ?? resolvedContext.projectName;
|
|
93
|
+
return {
|
|
94
|
+
...resolvedContext,
|
|
95
|
+
scopeId,
|
|
96
|
+
projectName,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function resolveProjectContext(options, apiState) {
|
|
100
|
+
const resolved = await resolveProjectBindingContext(options, {
|
|
101
|
+
requireProject: false,
|
|
102
|
+
});
|
|
103
|
+
const scopeId = normalizeNonEmpty(options.scopeId) ?? resolveOwnerScopeIdFromBinding(resolved);
|
|
104
|
+
return hydrateProjectContext(apiState, options, {
|
|
105
|
+
...resolved,
|
|
106
|
+
scopeId,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function buildResolvedContextRecord(context) {
|
|
110
|
+
return compactPayload({
|
|
111
|
+
repoRoot: context.repoRoot,
|
|
112
|
+
configPath: context.configPath,
|
|
113
|
+
configFound: context.configFound,
|
|
114
|
+
scopeId: context.scopeId,
|
|
115
|
+
projectId: context.projectId,
|
|
116
|
+
projectName: context.projectName,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function collectProjectArtifacts(result) {
|
|
120
|
+
const root = unwrapResultData(result) ?? (isRecord(result) ? result : {});
|
|
121
|
+
const artifacts = {};
|
|
122
|
+
const projectId = normalizeNonEmpty(root.projectId) ?? normalizeNonEmpty(root.id);
|
|
123
|
+
const scopeId = normalizeNonEmpty(root.scopeId);
|
|
124
|
+
if (projectId)
|
|
125
|
+
artifacts.projectId = projectId;
|
|
126
|
+
if (scopeId)
|
|
127
|
+
artifacts.scopeId = scopeId;
|
|
128
|
+
return Object.keys(artifacts).length > 0 ? artifacts : undefined;
|
|
129
|
+
}
|
|
130
|
+
function ensureProjectId(id, label = '--id') {
|
|
131
|
+
const resolved = normalizeNonEmpty(id);
|
|
132
|
+
if (!resolved)
|
|
133
|
+
throw new Error(`Provide ${label}.`);
|
|
134
|
+
return resolved;
|
|
135
|
+
}
|
|
136
|
+
function normalizeAuthoringMode(value) {
|
|
137
|
+
if (value === 'local' || value === 'hosted-only')
|
|
138
|
+
return value;
|
|
139
|
+
throw new Error('Provide --mode local or --mode hosted-only.');
|
|
140
|
+
}
|
|
141
|
+
function isArchivedProject(project) {
|
|
142
|
+
const status = normalizeNonEmpty(project.status)?.toLowerCase();
|
|
143
|
+
return status === 'archived' || status === 'deleted' || status === 'removed';
|
|
144
|
+
}
|
|
145
|
+
function projectSlugMatches(project, slug) {
|
|
146
|
+
return normalizeNonEmpty(project.slug)?.toLowerCase() === slug.toLowerCase();
|
|
147
|
+
}
|
|
148
|
+
function projectIdFromRecord(project) {
|
|
149
|
+
return ensureProjectId(normalizeNonEmpty(project.id) ?? normalizeNonEmpty(project.projectId), 'hosted project id');
|
|
150
|
+
}
|
|
151
|
+
async function resolveHostedProjectForLink(apiState, options, resolvedContext, slug) {
|
|
152
|
+
const explicitProjectId = normalizeNonEmpty(options.projectId);
|
|
153
|
+
if (explicitProjectId) {
|
|
154
|
+
const payload = await invokeHostedToolWithApiState(apiState, {
|
|
155
|
+
...buildGatewayOptions(options, resolvedContext),
|
|
156
|
+
toolId: 'agentspace.project.get-by-id',
|
|
157
|
+
input: { id: explicitProjectId },
|
|
158
|
+
});
|
|
159
|
+
const project = unwrapResultData(unwrapHostedToolResult(payload));
|
|
160
|
+
if (!isRecord(project))
|
|
161
|
+
throw new Error(`Hosted project "${explicitProjectId}" was not found.`);
|
|
162
|
+
const hostedSlug = normalizeNonEmpty(project.slug);
|
|
163
|
+
if (hostedSlug && hostedSlug.toLowerCase() !== slug.toLowerCase()) {
|
|
164
|
+
throw new Error(`Hosted project "${explicitProjectId}" has slug "${hostedSlug}", not "${slug}".`);
|
|
165
|
+
}
|
|
166
|
+
return project;
|
|
167
|
+
}
|
|
168
|
+
const payload = await invokeHostedToolWithApiState(apiState, {
|
|
169
|
+
...buildGatewayOptions(options, resolvedContext),
|
|
170
|
+
toolId: 'agentspace.project.list-projects',
|
|
171
|
+
input: {
|
|
172
|
+
filter: { slug },
|
|
173
|
+
options: { limit: 10 },
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const matches = unwrapListItems(unwrapHostedToolResult(payload)).filter((project) => projectSlugMatches(project, slug));
|
|
177
|
+
if (matches.length === 0) {
|
|
178
|
+
throw new Error(`Hosted project slug "${slug}" was not found. Create the hosted project first or pass --project-id for an exact project.`);
|
|
179
|
+
}
|
|
180
|
+
if (matches.length > 1) {
|
|
181
|
+
const ids = matches.map((project) => projectIdFromRecord(project)).join(', ');
|
|
182
|
+
throw new Error(`Hosted project slug "${slug}" is ambiguous: ${ids}. Pass --project-id.`);
|
|
183
|
+
}
|
|
184
|
+
return matches[0];
|
|
185
|
+
}
|
|
186
|
+
function ensureLinkableProject(project, slug) {
|
|
187
|
+
if (isArchivedProject(project)) {
|
|
188
|
+
throw new Error(`Hosted project slug "${slug}" is archived/deleted and cannot be linked for authoring.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function upsertProjectLink(config, params) {
|
|
192
|
+
const projectId = projectIdFromRecord(params.project);
|
|
193
|
+
const name = normalizeNonEmpty(params.project.name) ?? params.slug;
|
|
194
|
+
const hostedSlug = normalizeNonEmpty(params.project.slug) ?? params.slug;
|
|
195
|
+
const nextProject = {
|
|
196
|
+
name,
|
|
197
|
+
projectId,
|
|
198
|
+
slug: hostedSlug,
|
|
199
|
+
authoringMode: params.mode,
|
|
200
|
+
...(params.mode === 'local' && params.localRoot ? { localRoot: params.localRoot } : {}),
|
|
201
|
+
...(params.ownerRepo ? { ownerRepo: params.ownerRepo } : {}),
|
|
202
|
+
...(params.parentProjectSlug ? { parentProjectSlug: params.parentProjectSlug } : {}),
|
|
203
|
+
};
|
|
204
|
+
const projects = [...config.projects];
|
|
205
|
+
const existingIndex = projects.findIndex((project) => {
|
|
206
|
+
const existingSlug = normalizeNonEmpty(project.slug)?.toLowerCase();
|
|
207
|
+
return project.projectId === projectId || existingSlug === hostedSlug.toLowerCase() || existingSlug === params.slug.toLowerCase();
|
|
208
|
+
});
|
|
209
|
+
if (existingIndex >= 0) {
|
|
210
|
+
projects[existingIndex] = {
|
|
211
|
+
...projects[existingIndex],
|
|
212
|
+
...nextProject,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
projects.push(nextProject);
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
...config,
|
|
220
|
+
projects,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function slashPath(value) {
|
|
224
|
+
return value.split(path.sep).join('/');
|
|
225
|
+
}
|
|
226
|
+
function repoRelative(repoRoot, filePath) {
|
|
227
|
+
return slashPath(path.relative(repoRoot, filePath));
|
|
228
|
+
}
|
|
229
|
+
function normalizeMigrationLocalRoot(value) {
|
|
230
|
+
const localRoot = normalizeNonEmpty(value);
|
|
231
|
+
if (!localRoot)
|
|
232
|
+
throw new Error('Provide --local-root <path>.');
|
|
233
|
+
if (path.isAbsolute(localRoot))
|
|
234
|
+
throw new Error('--local-root must be repo-relative.');
|
|
235
|
+
const normalized = slashPath(path.normalize(localRoot));
|
|
236
|
+
if (normalized === '.' || normalized.startsWith('../') || normalized === '..') {
|
|
237
|
+
throw new Error('--local-root must stay inside the repo.');
|
|
238
|
+
}
|
|
239
|
+
if (!normalized.startsWith('.aops/projects/')) {
|
|
240
|
+
throw new Error('--local-root must be under .aops/projects/<slug>.');
|
|
241
|
+
}
|
|
242
|
+
return normalized;
|
|
243
|
+
}
|
|
244
|
+
function resolveMigrationProject(config, projectSlug) {
|
|
245
|
+
const matches = config.projects.filter((project) => normalizeNonEmpty(project.slug)?.toLowerCase() === projectSlug.toLowerCase());
|
|
246
|
+
if (matches.length === 0) {
|
|
247
|
+
throw new Error(`Project slug "${projectSlug}" was not found in repo config.`);
|
|
248
|
+
}
|
|
249
|
+
if (matches.length > 1) {
|
|
250
|
+
throw new Error(`Project slug "${projectSlug}" is ambiguous in repo config.`);
|
|
251
|
+
}
|
|
252
|
+
return matches[0];
|
|
253
|
+
}
|
|
254
|
+
function withMigratedProjectLocalRoot(config, projectSlug, localRoot) {
|
|
255
|
+
return {
|
|
256
|
+
...config,
|
|
257
|
+
projects: config.projects.map((project) => {
|
|
258
|
+
if (normalizeNonEmpty(project.slug)?.toLowerCase() !== projectSlug.toLowerCase())
|
|
259
|
+
return project;
|
|
260
|
+
return {
|
|
261
|
+
...project,
|
|
262
|
+
authoringMode: 'local',
|
|
263
|
+
localRoot,
|
|
264
|
+
};
|
|
265
|
+
}),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function classifyMigrationFile(rootKey, relativePath) {
|
|
269
|
+
const parts = relativePath.split('/').filter(Boolean);
|
|
270
|
+
if (rootKey === 'projectman') {
|
|
271
|
+
const first = parts[0] ?? 'root';
|
|
272
|
+
if (first === 'kanban-tasks')
|
|
273
|
+
return 'projectman.kanban-tasks';
|
|
274
|
+
if (first === 'review-requests')
|
|
275
|
+
return 'projectman.review-requests';
|
|
276
|
+
if (first === 'tombstones')
|
|
277
|
+
return `projectman.tombstones.${parts[1] ?? 'root'}`;
|
|
278
|
+
if (first === 'utasks')
|
|
279
|
+
return 'projectman.utasks';
|
|
280
|
+
if (first === 'boards')
|
|
281
|
+
return 'projectman.boards';
|
|
282
|
+
if (first === 'sprints')
|
|
283
|
+
return 'projectman.sprints';
|
|
284
|
+
if (first === 'issues')
|
|
285
|
+
return 'projectman.issues';
|
|
286
|
+
if (first === 'feedback')
|
|
287
|
+
return 'projectman.feedback';
|
|
288
|
+
if (first === 'views')
|
|
289
|
+
return 'projectman.views';
|
|
290
|
+
return 'projectman.root-files';
|
|
291
|
+
}
|
|
292
|
+
const first = parts[0] ?? 'root';
|
|
293
|
+
const second = parts[1] ?? 'root';
|
|
294
|
+
if (first === 'memory') {
|
|
295
|
+
if (['items', 'by-durability', 'by-flow', 'by-owner', 'subjects'].includes(second))
|
|
296
|
+
return `agentspace.memory.${second}`;
|
|
297
|
+
return 'agentspace.memory.root-files';
|
|
298
|
+
}
|
|
299
|
+
if (first === 'experience') {
|
|
300
|
+
if (second === 'items' || second === 'views')
|
|
301
|
+
return `agentspace.experience.${second}`;
|
|
302
|
+
return 'agentspace.experience.root-files';
|
|
303
|
+
}
|
|
304
|
+
if (first === 'discussions') {
|
|
305
|
+
if (second === 'topics')
|
|
306
|
+
return 'agentspace.discussions.topics';
|
|
307
|
+
return 'agentspace.discussions.root-files';
|
|
308
|
+
}
|
|
309
|
+
if (first === 'collabs') {
|
|
310
|
+
if (second === 'sessions')
|
|
311
|
+
return 'agentspace.collabs.sessions';
|
|
312
|
+
return 'agentspace.collabs.root-files';
|
|
313
|
+
}
|
|
314
|
+
return 'agentspace.root-files';
|
|
315
|
+
}
|
|
316
|
+
async function collectMigrationFiles(params) {
|
|
317
|
+
async function walk(dir) {
|
|
318
|
+
let entries;
|
|
319
|
+
try {
|
|
320
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
const code = error.code;
|
|
324
|
+
if (code === 'ENOENT')
|
|
325
|
+
return [];
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
const files = [];
|
|
329
|
+
for (const entry of entries) {
|
|
330
|
+
const filePath = path.join(dir, entry.name);
|
|
331
|
+
if (entry.isDirectory()) {
|
|
332
|
+
files.push(...await walk(filePath));
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
336
|
+
continue;
|
|
337
|
+
const content = entry.isSymbolicLink()
|
|
338
|
+
? Buffer.from(await fs.readlink(filePath), 'utf8')
|
|
339
|
+
: await fs.readFile(filePath);
|
|
340
|
+
const kind = entry.isSymbolicLink() ? 'symlink' : 'file';
|
|
341
|
+
const relativePath = slashPath(path.relative(params.sourceRoot, filePath));
|
|
342
|
+
files.push({
|
|
343
|
+
rootKey: params.rootKey,
|
|
344
|
+
relativePath,
|
|
345
|
+
sourcePath: filePath,
|
|
346
|
+
targetPath: path.join(params.targetRoot, params.rootKey, relativePath),
|
|
347
|
+
kind,
|
|
348
|
+
category: classifyMigrationFile(params.rootKey, relativePath),
|
|
349
|
+
size: content.byteLength,
|
|
350
|
+
sha256: createHash('sha256').update(content).digest('hex'),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return files;
|
|
354
|
+
}
|
|
355
|
+
return (await walk(params.sourceRoot)).sort((left, right) => `${left.rootKey}/${left.relativePath}`.localeCompare(`${right.rootKey}/${right.relativePath}`));
|
|
356
|
+
}
|
|
357
|
+
async function directoryHasFiles(dir) {
|
|
358
|
+
try {
|
|
359
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
360
|
+
for (const entry of entries) {
|
|
361
|
+
const filePath = path.join(dir, entry.name);
|
|
362
|
+
if (entry.isDirectory()) {
|
|
363
|
+
if (await directoryHasFiles(filePath))
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
const code = error.code;
|
|
374
|
+
if (code === 'ENOENT')
|
|
375
|
+
return false;
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function summarizeMigrationFiles(files) {
|
|
380
|
+
const byRoot = {
|
|
381
|
+
projectman: { before: 0, plannedAfter: 0 },
|
|
382
|
+
agentspace: { before: 0, plannedAfter: 0 },
|
|
383
|
+
};
|
|
384
|
+
const byCategory = {};
|
|
385
|
+
let totalBytes = 0;
|
|
386
|
+
for (const file of files) {
|
|
387
|
+
totalBytes += file.size;
|
|
388
|
+
byRoot[file.rootKey] = byRoot[file.rootKey] ?? { before: 0, plannedAfter: 0 };
|
|
389
|
+
byRoot[file.rootKey].before += 1;
|
|
390
|
+
byRoot[file.rootKey].plannedAfter += 1;
|
|
391
|
+
byCategory[file.category] = byCategory[file.category] ?? { before: 0, plannedAfter: 0 };
|
|
392
|
+
byCategory[file.category].before += 1;
|
|
393
|
+
byCategory[file.category].plannedAfter += 1;
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
totalFiles: files.length,
|
|
397
|
+
totalBytes,
|
|
398
|
+
byRoot,
|
|
399
|
+
byCategory: Object.fromEntries(Object.entries(byCategory).sort(([left], [right]) => left.localeCompare(right))),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function aggregateMigrationChecksum(files) {
|
|
403
|
+
const hash = createHash('sha256');
|
|
404
|
+
for (const file of files) {
|
|
405
|
+
hash.update(file.rootKey);
|
|
406
|
+
hash.update('\0');
|
|
407
|
+
hash.update(file.relativePath);
|
|
408
|
+
hash.update('\0');
|
|
409
|
+
hash.update(file.sha256);
|
|
410
|
+
hash.update('\0');
|
|
411
|
+
hash.update(String(file.size));
|
|
412
|
+
hash.update('\0');
|
|
413
|
+
}
|
|
414
|
+
return hash.digest('hex');
|
|
415
|
+
}
|
|
416
|
+
async function buildLocalRootMigrationPlan(params) {
|
|
417
|
+
const targetRoot = path.join(params.repoRoot, params.localRoot);
|
|
418
|
+
const sourceRoots = {
|
|
419
|
+
projectman: path.join(params.repoRoot, '.aops', 'projectman'),
|
|
420
|
+
agentspace: path.join(params.repoRoot, '.aops', 'agentspace'),
|
|
421
|
+
};
|
|
422
|
+
const files = [
|
|
423
|
+
...await collectMigrationFiles({
|
|
424
|
+
repoRoot: params.repoRoot,
|
|
425
|
+
sourceRoot: sourceRoots.projectman,
|
|
426
|
+
targetRoot,
|
|
427
|
+
rootKey: 'projectman',
|
|
428
|
+
}),
|
|
429
|
+
...await collectMigrationFiles({
|
|
430
|
+
repoRoot: params.repoRoot,
|
|
431
|
+
sourceRoot: sourceRoots.agentspace,
|
|
432
|
+
targetRoot,
|
|
433
|
+
rootKey: 'agentspace',
|
|
434
|
+
}),
|
|
435
|
+
].sort((left, right) => `${left.rootKey}/${left.relativePath}`.localeCompare(`${right.rootKey}/${right.relativePath}`));
|
|
436
|
+
const counts = summarizeMigrationFiles(files);
|
|
437
|
+
const timestamp = params.timestamp ?? new Date().toISOString().replace(/[:.]/g, '-');
|
|
438
|
+
const conflicts = [];
|
|
439
|
+
if (await directoryHasFiles(targetRoot)) {
|
|
440
|
+
conflicts.push({
|
|
441
|
+
code: 'target_not_empty',
|
|
442
|
+
path: repoRelative(params.repoRoot, targetRoot),
|
|
443
|
+
message: 'Target localRoot already contains files; apply refuses unless a future resume/rollback flow matches a manifest.',
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
projectSlug: params.projectSlug,
|
|
448
|
+
localRoot: params.localRoot,
|
|
449
|
+
repoRoot: params.repoRoot,
|
|
450
|
+
configPath: params.configPath,
|
|
451
|
+
sourceRoots: {
|
|
452
|
+
projectman: repoRelative(params.repoRoot, sourceRoots.projectman),
|
|
453
|
+
agentspace: repoRelative(params.repoRoot, sourceRoots.agentspace),
|
|
454
|
+
},
|
|
455
|
+
targetRoot: repoRelative(params.repoRoot, targetRoot),
|
|
456
|
+
archiveRoot: `.aops/archive/localroot-migrations/${timestamp}`,
|
|
457
|
+
files,
|
|
458
|
+
counts,
|
|
459
|
+
checksum: {
|
|
460
|
+
aggregateSha256: aggregateMigrationChecksum(files),
|
|
461
|
+
},
|
|
462
|
+
conflicts,
|
|
463
|
+
warnings: files.length === 0 ? ['No files found under flat .aops/projectman or .aops/agentspace source roots.'] : [],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function publicMigrationSummary(plan, params) {
|
|
467
|
+
return {
|
|
468
|
+
mode: params.mode,
|
|
469
|
+
projectSlug: plan.projectSlug,
|
|
470
|
+
localRoot: plan.localRoot,
|
|
471
|
+
sourceRoots: plan.sourceRoots,
|
|
472
|
+
targetRoot: plan.targetRoot,
|
|
473
|
+
archiveRoot: plan.archiveRoot,
|
|
474
|
+
totals: {
|
|
475
|
+
sourceFileCount: plan.counts.totalFiles,
|
|
476
|
+
plannedTargetFileCount: plan.counts.totalFiles,
|
|
477
|
+
totalBytes: plan.counts.totalBytes,
|
|
478
|
+
},
|
|
479
|
+
counts: plan.counts,
|
|
480
|
+
checksum: plan.checksum,
|
|
481
|
+
conflicts: plan.conflicts,
|
|
482
|
+
warnings: plan.warnings,
|
|
483
|
+
guards: [
|
|
484
|
+
'dry-run is non-mutating',
|
|
485
|
+
'apply requires --apply --confirm',
|
|
486
|
+
'apply refuses non-empty target localRoot',
|
|
487
|
+
'config localRoot is written only after target copy verification',
|
|
488
|
+
'flat source roots move to migration archive after config switch',
|
|
489
|
+
],
|
|
490
|
+
...(params.applied ? { applied: params.applied } : {}),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
async function copyMigrationRoot(source, destination) {
|
|
494
|
+
if (!existsSync(source))
|
|
495
|
+
return;
|
|
496
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
497
|
+
await fs.cp(source, destination, { recursive: true, force: false, errorOnExist: true, verbatimSymlinks: true });
|
|
498
|
+
}
|
|
499
|
+
async function verifyCopiedMigrationPlan(plan, rootDir) {
|
|
500
|
+
for (const expected of plan.files) {
|
|
501
|
+
const filePath = path.join(rootDir, expected.rootKey, expected.relativePath);
|
|
502
|
+
const content = expected.kind === 'symlink'
|
|
503
|
+
? Buffer.from(await fs.readlink(filePath), 'utf8')
|
|
504
|
+
: await fs.readFile(filePath);
|
|
505
|
+
const sha256 = createHash('sha256').update(content).digest('hex');
|
|
506
|
+
if (sha256 !== expected.sha256 || content.byteLength !== expected.size) {
|
|
507
|
+
throw new Error(`Copied file verification failed for ${expected.rootKey}/${expected.relativePath}.`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function applyLocalRootMigration(params) {
|
|
512
|
+
if (params.plan.conflicts.length > 0) {
|
|
513
|
+
throw new Error(`Migration target has conflicts: ${params.plan.conflicts.map((conflict) => conflict.code).join(', ')}`);
|
|
514
|
+
}
|
|
515
|
+
const repoRoot = params.plan.repoRoot;
|
|
516
|
+
const archiveRoot = path.join(repoRoot, params.plan.archiveRoot);
|
|
517
|
+
const tempTargetRoot = path.join(repoRoot, '.aops', 'projects', `.tmp-${params.plan.projectSlug}-migrate-${path.basename(params.plan.archiveRoot)}`);
|
|
518
|
+
const targetRoot = path.join(repoRoot, params.plan.localRoot);
|
|
519
|
+
const sourceProjectman = path.join(repoRoot, '.aops', 'projectman');
|
|
520
|
+
const sourceAgentspace = path.join(repoRoot, '.aops', 'agentspace');
|
|
521
|
+
await fs.mkdir(archiveRoot, { recursive: true });
|
|
522
|
+
await fs.writeFile(path.join(archiveRoot, 'original-aops.config.json'), await fs.readFile(params.configPath, 'utf8'), 'utf8');
|
|
523
|
+
await fs.writeFile(path.join(archiveRoot, 'manifest.json'), `${JSON.stringify({
|
|
524
|
+
...publicMigrationSummary(params.plan, { mode: 'dry-run' }),
|
|
525
|
+
files: params.plan.files.map((file) => ({
|
|
526
|
+
rootKey: file.rootKey,
|
|
527
|
+
relativePath: file.relativePath,
|
|
528
|
+
kind: file.kind,
|
|
529
|
+
category: file.category,
|
|
530
|
+
size: file.size,
|
|
531
|
+
sha256: file.sha256,
|
|
532
|
+
})),
|
|
533
|
+
}, null, 2)}\n`, 'utf8');
|
|
534
|
+
await fs.rm(tempTargetRoot, { recursive: true, force: true });
|
|
535
|
+
await copyMigrationRoot(sourceProjectman, path.join(tempTargetRoot, 'projectman'));
|
|
536
|
+
await copyMigrationRoot(sourceAgentspace, path.join(tempTargetRoot, 'agentspace'));
|
|
537
|
+
await verifyCopiedMigrationPlan(params.plan, tempTargetRoot);
|
|
538
|
+
await fs.mkdir(path.dirname(targetRoot), { recursive: true });
|
|
539
|
+
await fs.rename(tempTargetRoot, targetRoot);
|
|
540
|
+
await writeAopsRepoConfig(params.configPath, withMigratedProjectLocalRoot(params.config, params.plan.projectSlug, params.plan.localRoot));
|
|
541
|
+
const archivedFlatRoot = path.join(archiveRoot, 'flat-roots');
|
|
542
|
+
await fs.mkdir(archivedFlatRoot, { recursive: true });
|
|
543
|
+
if (existsSync(sourceProjectman))
|
|
544
|
+
await fs.rename(sourceProjectman, path.join(archivedFlatRoot, 'projectman'));
|
|
545
|
+
if (existsSync(sourceAgentspace))
|
|
546
|
+
await fs.rename(sourceAgentspace, path.join(archivedFlatRoot, 'agentspace'));
|
|
547
|
+
return {
|
|
548
|
+
targetRoot: params.plan.targetRoot,
|
|
549
|
+
archiveRoot: params.plan.archiveRoot,
|
|
550
|
+
configPath: repoRelative(repoRoot, params.configPath),
|
|
551
|
+
movedFlatRootsTo: repoRelative(repoRoot, archivedFlatRoot),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
async function invokeProjectTool(apiState, options, resolvedContext, params) {
|
|
555
|
+
ensureGuardedWrite(options, 'This command mutates hosted project state.');
|
|
556
|
+
const payload = await invokeHostedToolWithApiState(apiState, {
|
|
557
|
+
...buildGatewayOptions(options, resolvedContext),
|
|
558
|
+
tenantId: options.tenantId,
|
|
559
|
+
locale: options.locale,
|
|
560
|
+
fallbackLocale: options.fallbackLocale,
|
|
561
|
+
timeoutMs: options.timeoutMs,
|
|
562
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
563
|
+
accessToken: options.accessToken,
|
|
564
|
+
refreshToken: options.refreshToken,
|
|
565
|
+
toolId: params.toolId,
|
|
566
|
+
input: params.input,
|
|
567
|
+
preview: options.preview,
|
|
568
|
+
apply: options.apply,
|
|
569
|
+
confirm: options.confirm,
|
|
570
|
+
idempotencyKey: options.idempotencyKey,
|
|
571
|
+
});
|
|
572
|
+
const result = unwrapHostedToolResult(payload);
|
|
573
|
+
if (options.json) {
|
|
574
|
+
console.log(JSON.stringify(buildHostedSugarEnvelope({
|
|
575
|
+
command: params.command,
|
|
576
|
+
toolId: params.toolId,
|
|
577
|
+
resolvedContext: buildResolvedContextRecord(resolvedContext),
|
|
578
|
+
input: params.input,
|
|
579
|
+
artifacts: collectProjectArtifacts(result),
|
|
580
|
+
result,
|
|
581
|
+
}), null, 2));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
logSuccess(params.successText);
|
|
585
|
+
console.log(JSON.stringify(result, null, 2));
|
|
586
|
+
}
|
|
587
|
+
function printProjectTable(result) {
|
|
588
|
+
const rows = unwrapListItems(result);
|
|
589
|
+
console.log(renderTable({
|
|
590
|
+
columns: [
|
|
591
|
+
{ header: 'Name', maxWidth: 28 },
|
|
592
|
+
{ header: 'Slug', maxWidth: 28 },
|
|
593
|
+
{ header: 'Status', maxWidth: 12 },
|
|
594
|
+
{ header: 'Visibility', maxWidth: 12 },
|
|
595
|
+
{ header: 'ID', maxWidth: 36 },
|
|
596
|
+
],
|
|
597
|
+
rows: rows.map((row) => [
|
|
598
|
+
normalizeNonEmpty(row.name) ?? String(row.name ?? '-'),
|
|
599
|
+
normalizeNonEmpty(row.slug) ?? String(row.slug ?? '-'),
|
|
600
|
+
normalizeNonEmpty(row.status) ?? String(row.status ?? '-'),
|
|
601
|
+
normalizeNonEmpty(row.visibility) ?? String(row.visibility ?? '-'),
|
|
602
|
+
normalizeNonEmpty(row.id) ?? String(row.id ?? '-'),
|
|
603
|
+
]),
|
|
604
|
+
emptyText: '(no projects found)',
|
|
605
|
+
}));
|
|
606
|
+
}
|
|
607
|
+
export async function runProjectList(options = {}) {
|
|
608
|
+
try {
|
|
609
|
+
const apiState = await requireApiState(options);
|
|
610
|
+
if (!apiState)
|
|
611
|
+
return;
|
|
612
|
+
const resolvedContext = await resolveProjectContext(options, apiState);
|
|
613
|
+
const input = {
|
|
614
|
+
filter: compactPayload({
|
|
615
|
+
name: normalizeNonEmpty(options.name),
|
|
616
|
+
slug: normalizeNonEmpty(options.slug),
|
|
617
|
+
status: normalizeNonEmpty(options.status),
|
|
618
|
+
visibility: normalizeNonEmpty(options.visibility),
|
|
619
|
+
projectType: normalizeNonEmpty(options.projectType),
|
|
620
|
+
}),
|
|
621
|
+
options: compactPayload({
|
|
622
|
+
limit: options.limit !== undefined ? toInteger(options.limit, '--limit') : undefined,
|
|
623
|
+
}),
|
|
624
|
+
};
|
|
625
|
+
const payload = await invokeHostedToolWithApiState(apiState, {
|
|
626
|
+
...buildGatewayOptions(options, resolvedContext),
|
|
627
|
+
tenantId: options.tenantId,
|
|
628
|
+
locale: options.locale,
|
|
629
|
+
fallbackLocale: options.fallbackLocale,
|
|
630
|
+
timeoutMs: options.timeoutMs,
|
|
631
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
632
|
+
accessToken: options.accessToken,
|
|
633
|
+
refreshToken: options.refreshToken,
|
|
634
|
+
toolId: 'agentspace.project.list-projects',
|
|
635
|
+
input,
|
|
636
|
+
});
|
|
637
|
+
const result = unwrapHostedToolResult(payload);
|
|
638
|
+
if (options.json) {
|
|
639
|
+
console.log(JSON.stringify(buildHostedSugarEnvelope({
|
|
640
|
+
command: 'project.list',
|
|
641
|
+
toolId: 'agentspace.project.list-projects',
|
|
642
|
+
resolvedContext: buildResolvedContextRecord(resolvedContext),
|
|
643
|
+
input,
|
|
644
|
+
result,
|
|
645
|
+
}), null, 2));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
logSuccess('Project list loaded.');
|
|
649
|
+
printProjectTable(result);
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
653
|
+
process.exitCode = 1;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
export async function runProjectGet(options = {}) {
|
|
657
|
+
try {
|
|
658
|
+
const apiState = await requireApiState(options);
|
|
659
|
+
if (!apiState)
|
|
660
|
+
return;
|
|
661
|
+
const resolvedContext = await resolveProjectContext(options, apiState);
|
|
662
|
+
const input = { id: ensureProjectId(options.id) };
|
|
663
|
+
const payload = await invokeHostedToolWithApiState(apiState, {
|
|
664
|
+
...buildGatewayOptions(options, resolvedContext),
|
|
665
|
+
tenantId: options.tenantId,
|
|
666
|
+
locale: options.locale,
|
|
667
|
+
fallbackLocale: options.fallbackLocale,
|
|
668
|
+
timeoutMs: options.timeoutMs,
|
|
669
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
670
|
+
accessToken: options.accessToken,
|
|
671
|
+
refreshToken: options.refreshToken,
|
|
672
|
+
toolId: 'agentspace.project.get-project',
|
|
673
|
+
input,
|
|
674
|
+
});
|
|
675
|
+
const result = unwrapHostedToolResult(payload);
|
|
676
|
+
if (options.json) {
|
|
677
|
+
console.log(JSON.stringify(buildHostedSugarEnvelope({
|
|
678
|
+
command: 'project.get',
|
|
679
|
+
toolId: 'agentspace.project.get-project',
|
|
680
|
+
resolvedContext: buildResolvedContextRecord(resolvedContext),
|
|
681
|
+
input,
|
|
682
|
+
artifacts: collectProjectArtifacts(result),
|
|
683
|
+
result,
|
|
684
|
+
}), null, 2));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
logSuccess('Project loaded.');
|
|
688
|
+
console.log(JSON.stringify(result, null, 2));
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
692
|
+
process.exitCode = 1;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
export async function runProjectCreate(options = {}) {
|
|
696
|
+
try {
|
|
697
|
+
const apiState = await requireApiState(options);
|
|
698
|
+
if (!apiState)
|
|
699
|
+
return;
|
|
700
|
+
const resolvedContext = await resolveProjectContext(options, apiState);
|
|
701
|
+
const seed = parseJsonSeed(options.input);
|
|
702
|
+
const name = resolveStringField(options.name, seed, 'name');
|
|
703
|
+
if (!name)
|
|
704
|
+
throw new Error('Project create requires --name or input.name.');
|
|
705
|
+
const input = {
|
|
706
|
+
data: compactPayload({
|
|
707
|
+
name,
|
|
708
|
+
description: resolveStringField(options.description, seed, 'description'),
|
|
709
|
+
slug: resolveStringField(options.slug, seed, 'slug'),
|
|
710
|
+
status: resolveStringField(options.status, seed, 'status'),
|
|
711
|
+
visibility: resolveStringField(options.visibility, seed, 'visibility'),
|
|
712
|
+
projectType: resolveStringField(options.projectType, seed, 'projectType'),
|
|
713
|
+
}),
|
|
714
|
+
};
|
|
715
|
+
await invokeProjectTool(apiState, options, resolvedContext, {
|
|
716
|
+
command: 'project.create',
|
|
717
|
+
toolId: 'agentspace.project.create',
|
|
718
|
+
input,
|
|
719
|
+
successText: 'Project created.',
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
724
|
+
process.exitCode = 1;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
export async function runProjectUpdate(options = {}) {
|
|
728
|
+
try {
|
|
729
|
+
const apiState = await requireApiState(options);
|
|
730
|
+
if (!apiState)
|
|
731
|
+
return;
|
|
732
|
+
const resolvedContext = await resolveProjectContext(options, apiState);
|
|
733
|
+
const seed = parseJsonSeed(options.input);
|
|
734
|
+
const id = ensureProjectId(resolveStringField(options.id, seed, 'id'));
|
|
735
|
+
const patch = compactPayload({
|
|
736
|
+
name: resolveStringField(options.name, seed, 'name'),
|
|
737
|
+
description: resolveStringField(options.description, seed, 'description'),
|
|
738
|
+
slug: resolveStringField(options.slug, seed, 'slug'),
|
|
739
|
+
status: resolveStringField(options.status, seed, 'status'),
|
|
740
|
+
visibility: resolveStringField(options.visibility, seed, 'visibility'),
|
|
741
|
+
projectType: resolveStringField(options.projectType, seed, 'projectType'),
|
|
742
|
+
});
|
|
743
|
+
if (Object.keys(patch).length === 0) {
|
|
744
|
+
throw new Error('Provide at least one project field to update.');
|
|
745
|
+
}
|
|
746
|
+
const input = { id, patch };
|
|
747
|
+
await invokeProjectTool(apiState, options, resolvedContext, {
|
|
748
|
+
command: 'project.update',
|
|
749
|
+
toolId: 'agentspace.project.update-project',
|
|
750
|
+
input,
|
|
751
|
+
successText: 'Project updated.',
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
756
|
+
process.exitCode = 1;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
export async function runProjectDelete(options = {}) {
|
|
760
|
+
try {
|
|
761
|
+
ensureDestructiveWrite(options, 'This command deletes hosted projects.');
|
|
762
|
+
const apiState = await requireApiState(options);
|
|
763
|
+
if (!apiState)
|
|
764
|
+
return;
|
|
765
|
+
const resolvedContext = await resolveProjectContext(options, apiState);
|
|
766
|
+
const input = { id: ensureProjectId(options.id) };
|
|
767
|
+
await invokeProjectTool(apiState, options, resolvedContext, {
|
|
768
|
+
command: 'project.delete',
|
|
769
|
+
toolId: 'agentspace.project.remove-project',
|
|
770
|
+
input,
|
|
771
|
+
successText: 'Project deleted.',
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
776
|
+
process.exitCode = 1;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
export async function runProjectLink(options = {}) {
|
|
780
|
+
try {
|
|
781
|
+
ensureGuardedWrite(options, 'This command mutates repo project link config.');
|
|
782
|
+
const slug = normalizeNonEmpty(options.slug);
|
|
783
|
+
if (!slug)
|
|
784
|
+
throw new Error('Provide --slug <slug>.');
|
|
785
|
+
const mode = normalizeAuthoringMode(options.mode);
|
|
786
|
+
const localRoot = normalizeNonEmpty(options.localRoot);
|
|
787
|
+
if (mode === 'local' && !localRoot) {
|
|
788
|
+
throw new Error('Local project links require --local-root <path>.');
|
|
789
|
+
}
|
|
790
|
+
const loaded = await loadAopsRepoConfig(process.cwd());
|
|
791
|
+
if (!loaded.config) {
|
|
792
|
+
throw new Error(`Repo config was not found at ${loaded.configPath}. Run \`aops-cli init\` first.`);
|
|
793
|
+
}
|
|
794
|
+
const apiState = await requireApiState(options);
|
|
795
|
+
if (!apiState)
|
|
796
|
+
return;
|
|
797
|
+
const resolvedContext = await resolveProjectContext(options, apiState);
|
|
798
|
+
const project = await resolveHostedProjectForLink(apiState, options, resolvedContext, slug);
|
|
799
|
+
ensureLinkableProject(project, slug);
|
|
800
|
+
const nextConfig = upsertProjectLink(loaded.config, {
|
|
801
|
+
slug,
|
|
802
|
+
project,
|
|
803
|
+
mode,
|
|
804
|
+
localRoot,
|
|
805
|
+
ownerRepo: normalizeNonEmpty(options.ownerRepo),
|
|
806
|
+
parentProjectSlug: normalizeNonEmpty(options.parentProjectSlug),
|
|
807
|
+
});
|
|
808
|
+
const result = {
|
|
809
|
+
preview: options.preview === true,
|
|
810
|
+
configPath: loaded.configPath,
|
|
811
|
+
link: nextConfig.projects.find((entry) => normalizeNonEmpty(entry.slug)?.toLowerCase() === slug.toLowerCase()),
|
|
812
|
+
};
|
|
813
|
+
if (options.preview !== true) {
|
|
814
|
+
await writeAopsRepoConfig(loaded.configPath, nextConfig);
|
|
815
|
+
}
|
|
816
|
+
if (options.json) {
|
|
817
|
+
console.log(JSON.stringify(buildHostedSugarEnvelope({
|
|
818
|
+
command: 'project.link',
|
|
819
|
+
surface: 'repo-config',
|
|
820
|
+
resolvedContext: buildResolvedContextRecord(resolvedContext),
|
|
821
|
+
input: compactPayload({
|
|
822
|
+
slug,
|
|
823
|
+
mode,
|
|
824
|
+
projectId: normalizeNonEmpty(options.projectId),
|
|
825
|
+
localRoot,
|
|
826
|
+
ownerRepo: normalizeNonEmpty(options.ownerRepo),
|
|
827
|
+
parentProjectSlug: normalizeNonEmpty(options.parentProjectSlug),
|
|
828
|
+
}),
|
|
829
|
+
artifacts: { projectId: projectIdFromRecord(project) },
|
|
830
|
+
result,
|
|
831
|
+
}), null, 2));
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
logSuccess(options.preview === true ? 'Project link preview ready.' : 'Project link saved.');
|
|
835
|
+
console.log(JSON.stringify(result, null, 2));
|
|
836
|
+
}
|
|
837
|
+
catch (error) {
|
|
838
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
839
|
+
process.exitCode = 1;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
export async function runProjectLinksList(options = {}) {
|
|
843
|
+
try {
|
|
844
|
+
const loaded = await loadAopsRepoConfig(process.cwd());
|
|
845
|
+
if (!loaded.config) {
|
|
846
|
+
throw new Error(`Repo config was not found at ${loaded.configPath}. Run \`aops-cli init\` first.`);
|
|
847
|
+
}
|
|
848
|
+
const links = loaded.config.projects.map((project) => compactPayload({
|
|
849
|
+
name: project.name,
|
|
850
|
+
slug: project.slug,
|
|
851
|
+
projectId: project.projectId,
|
|
852
|
+
authoringMode: project.authoringMode ?? 'local',
|
|
853
|
+
localRoot: project.localRoot,
|
|
854
|
+
ownerRepo: project.ownerRepo,
|
|
855
|
+
parentProjectSlug: project.parentProjectSlug,
|
|
856
|
+
}));
|
|
857
|
+
const result = { configPath: loaded.configPath, data: links };
|
|
858
|
+
if (options.json) {
|
|
859
|
+
console.log(JSON.stringify(buildHostedSugarEnvelope({
|
|
860
|
+
command: 'project.links.list',
|
|
861
|
+
surface: 'repo-config',
|
|
862
|
+
resolvedContext: {
|
|
863
|
+
repoRoot: loaded.rootDir,
|
|
864
|
+
configPath: loaded.configPath,
|
|
865
|
+
configFound: true,
|
|
866
|
+
},
|
|
867
|
+
input: {},
|
|
868
|
+
result,
|
|
869
|
+
}), null, 2));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
console.log(renderTable({
|
|
873
|
+
columns: [
|
|
874
|
+
{ header: 'Slug', maxWidth: 28 },
|
|
875
|
+
{ header: 'Mode', maxWidth: 14 },
|
|
876
|
+
{ header: 'Project ID', maxWidth: 36 },
|
|
877
|
+
{ header: 'Local Root', maxWidth: 32 },
|
|
878
|
+
],
|
|
879
|
+
rows: links.map((link) => [
|
|
880
|
+
String(link.slug ?? '-'),
|
|
881
|
+
String(link.authoringMode ?? 'local'),
|
|
882
|
+
String(link.projectId ?? '-'),
|
|
883
|
+
String(link.localRoot ?? '-'),
|
|
884
|
+
]),
|
|
885
|
+
emptyText: '(no project links found)',
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
890
|
+
process.exitCode = 1;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
export async function runProjectMigrateLocalRoot(options = {}) {
|
|
894
|
+
try {
|
|
895
|
+
const projectSlug = normalizeNonEmpty(options.projectSlug);
|
|
896
|
+
if (!projectSlug)
|
|
897
|
+
throw new Error('Provide --project-slug <slug>.');
|
|
898
|
+
const localRoot = normalizeMigrationLocalRoot(options.localRoot);
|
|
899
|
+
if (options.dryRun === true && options.apply === true) {
|
|
900
|
+
throw new Error('Use either --dry-run or --apply, not both.');
|
|
901
|
+
}
|
|
902
|
+
const loaded = await loadAopsRepoConfig(process.cwd());
|
|
903
|
+
if (!loaded.config) {
|
|
904
|
+
throw new Error(`Repo config was not found at ${loaded.configPath}. Run \`aops-cli init\` first.`);
|
|
905
|
+
}
|
|
906
|
+
const project = resolveMigrationProject(loaded.config, projectSlug);
|
|
907
|
+
const currentLocalRoot = normalizeNonEmpty(project.localRoot);
|
|
908
|
+
if (currentLocalRoot && currentLocalRoot !== localRoot) {
|
|
909
|
+
throw new Error(`Project "${projectSlug}" already has localRoot "${currentLocalRoot}".`);
|
|
910
|
+
}
|
|
911
|
+
const plan = await buildLocalRootMigrationPlan({
|
|
912
|
+
repoRoot: loaded.rootDir,
|
|
913
|
+
configPath: loaded.configPath,
|
|
914
|
+
projectSlug,
|
|
915
|
+
localRoot,
|
|
916
|
+
timestamp: normalizeNonEmpty(options.timestamp),
|
|
917
|
+
});
|
|
918
|
+
const mode = options.apply === true ? 'apply' : 'dry-run';
|
|
919
|
+
if (mode === 'apply') {
|
|
920
|
+
if (options.confirm !== true) {
|
|
921
|
+
throw new Error('project migrate-local-root is destructive. Retry with --apply --confirm after reviewing dry-run output.');
|
|
922
|
+
}
|
|
923
|
+
const applied = await applyLocalRootMigration({
|
|
924
|
+
config: loaded.config,
|
|
925
|
+
configPath: loaded.configPath,
|
|
926
|
+
plan,
|
|
927
|
+
});
|
|
928
|
+
const result = publicMigrationSummary(plan, { mode, applied });
|
|
929
|
+
if (options.json) {
|
|
930
|
+
console.log(JSON.stringify(buildHostedSugarEnvelope({
|
|
931
|
+
command: 'project.migrate-local-root',
|
|
932
|
+
surface: 'repo-config-localroot-migration',
|
|
933
|
+
resolvedContext: {
|
|
934
|
+
repoRoot: loaded.rootDir,
|
|
935
|
+
configPath: loaded.configPath,
|
|
936
|
+
configFound: true,
|
|
937
|
+
projectSlug,
|
|
938
|
+
},
|
|
939
|
+
input: { projectSlug, localRoot, apply: true, confirm: true },
|
|
940
|
+
result,
|
|
941
|
+
}), null, 2));
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
logSuccess('Local root migration applied.');
|
|
945
|
+
console.log(JSON.stringify(result, null, 2));
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const result = publicMigrationSummary(plan, { mode });
|
|
949
|
+
if (options.json) {
|
|
950
|
+
console.log(JSON.stringify(buildHostedSugarEnvelope({
|
|
951
|
+
command: 'project.migrate-local-root',
|
|
952
|
+
surface: 'repo-config-localroot-migration',
|
|
953
|
+
resolvedContext: {
|
|
954
|
+
repoRoot: loaded.rootDir,
|
|
955
|
+
configPath: loaded.configPath,
|
|
956
|
+
configFound: true,
|
|
957
|
+
projectSlug,
|
|
958
|
+
},
|
|
959
|
+
input: { projectSlug, localRoot, dryRun: true },
|
|
960
|
+
result,
|
|
961
|
+
}), null, 2));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
logSuccess('Local root migration dry-run ready.');
|
|
965
|
+
console.log(JSON.stringify(result, null, 2));
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
969
|
+
process.exitCode = 1;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
function applyProjectContextOptions(cmd, params = {}) {
|
|
973
|
+
applyCommonOptions(cmd);
|
|
974
|
+
cmd.option('--project-id <id>', 'Project id used to resolve repo-bound project context');
|
|
975
|
+
cmd.option('--project-name <name>', 'Project name used to resolve repo-bound project context');
|
|
976
|
+
cmd.option('--scope-id <id>', 'Explicit scope id override');
|
|
977
|
+
if (params.withScopeResolution) {
|
|
978
|
+
cmd.option('--scope-resolution <mode>', 'Scope resolution policy: explicit or cascade');
|
|
979
|
+
}
|
|
980
|
+
return cmd;
|
|
981
|
+
}
|
|
982
|
+
function applyWriteGuards(cmd, params = {}) {
|
|
983
|
+
cmd.option('--preview', 'Return a validated preflight summary without executing the tool');
|
|
984
|
+
cmd.option('--apply', 'Execute the hosted write operation');
|
|
985
|
+
if (params.destructive) {
|
|
986
|
+
cmd.option('--confirm', 'Required with --apply for destructive deletes');
|
|
987
|
+
}
|
|
988
|
+
cmd.option('--idempotency-key <key>', 'Optional idempotency key for hosted writes');
|
|
989
|
+
return cmd;
|
|
990
|
+
}
|
|
991
|
+
function applyJsonSeedOption(cmd) {
|
|
992
|
+
cmd.option('--input <jsonOrFile>', 'Optional JSON object seed or @file.json');
|
|
993
|
+
return cmd;
|
|
994
|
+
}
|
|
995
|
+
export function makeProjectCommand() {
|
|
996
|
+
const cmd = new Command('project').description('Agentspace project sugar commands over the hosted AOPS gateway');
|
|
997
|
+
applyProjectContextOptions(cmd.command('list')
|
|
998
|
+
.description('List hosted projects')
|
|
999
|
+
.option('--name <text>', 'Project name filter')
|
|
1000
|
+
.option('--slug <text>', 'Project slug filter')
|
|
1001
|
+
.option('--status <status>', 'Project status filter')
|
|
1002
|
+
.option('--visibility <visibility>', 'Project visibility filter')
|
|
1003
|
+
.option('--project-type <type>', 'Project type filter')
|
|
1004
|
+
.option('--limit <n>', 'Optional item limit')).action(async (options) => {
|
|
1005
|
+
await runProjectList(options);
|
|
1006
|
+
});
|
|
1007
|
+
applyProjectContextOptions(cmd.command('get')
|
|
1008
|
+
.description('Get a project by id')
|
|
1009
|
+
.requiredOption('--id <id>', 'Project id')).action(async (options) => {
|
|
1010
|
+
await runProjectGet(options);
|
|
1011
|
+
});
|
|
1012
|
+
applyWriteGuards(applyJsonSeedOption(applyProjectContextOptions(cmd.command('create')
|
|
1013
|
+
.description('Create a project')
|
|
1014
|
+
.requiredOption('--name <text>', 'Project name')
|
|
1015
|
+
.option('--description <text>', 'Project description')
|
|
1016
|
+
.option('--slug <text>', 'Project slug')
|
|
1017
|
+
.option('--status <status>', 'Project status')
|
|
1018
|
+
.option('--visibility <visibility>', 'Project visibility')
|
|
1019
|
+
.option('--project-type <type>', 'Project type')))).action(async (options) => {
|
|
1020
|
+
await runProjectCreate(options);
|
|
1021
|
+
});
|
|
1022
|
+
applyWriteGuards(applyJsonSeedOption(applyProjectContextOptions(cmd.command('update')
|
|
1023
|
+
.description('Update a project')
|
|
1024
|
+
.requiredOption('--id <id>', 'Project id')
|
|
1025
|
+
.option('--name <text>', 'Project name')
|
|
1026
|
+
.option('--description <text>', 'Project description')
|
|
1027
|
+
.option('--slug <text>', 'Project slug')
|
|
1028
|
+
.option('--status <status>', 'Project status')
|
|
1029
|
+
.option('--visibility <visibility>', 'Project visibility')
|
|
1030
|
+
.option('--project-type <type>', 'Project type')))).action(async (options) => {
|
|
1031
|
+
await runProjectUpdate(options);
|
|
1032
|
+
});
|
|
1033
|
+
applyWriteGuards(applyProjectContextOptions(cmd.command('delete')
|
|
1034
|
+
.description('Delete a project')
|
|
1035
|
+
.requiredOption('--id <id>', 'Project id')), { destructive: true }).action(async (options) => {
|
|
1036
|
+
await runProjectDelete(options);
|
|
1037
|
+
});
|
|
1038
|
+
applyWriteGuards(applyProjectContextOptions(cmd.command('link')
|
|
1039
|
+
.description('Link a hosted project slug into the repo project registry')
|
|
1040
|
+
.requiredOption('--slug <slug>', 'Hosted project slug')
|
|
1041
|
+
.requiredOption('--mode <mode>', 'Authoring mode: local or hosted-only')
|
|
1042
|
+
.option('--local-root <path>', 'Local repo-first root for --mode local')
|
|
1043
|
+
.option('--owner-repo <name>', 'Optional owner repo metadata')
|
|
1044
|
+
.option('--parent-project-slug <slug>', 'Optional parent project slug metadata'))).action(async (options) => {
|
|
1045
|
+
await runProjectLink(options);
|
|
1046
|
+
});
|
|
1047
|
+
applyProjectContextOptions(cmd.command('migrate-local-root')
|
|
1048
|
+
.description('Plan or apply flat .aops projectman/agentspace migration into .aops/projects/<slug>')
|
|
1049
|
+
.requiredOption('--project-slug <slug>', 'Repo project slug to migrate')
|
|
1050
|
+
.requiredOption('--local-root <path>', 'Target local root, normally .aops/projects/<slug>')
|
|
1051
|
+
.option('--dry-run', 'Build the migration summary without mutating files')
|
|
1052
|
+
.option('--apply', 'Apply the migration after dry-run review')
|
|
1053
|
+
.option('--confirm', 'Required with --apply because flat roots are moved to an archive')
|
|
1054
|
+
.option('--timestamp <value>', 'Optional deterministic archive timestamp for tests/replay')).action(async (options) => {
|
|
1055
|
+
await runProjectMigrateLocalRoot(options);
|
|
1056
|
+
});
|
|
1057
|
+
const links = cmd.command('links').description('List repo project registry links');
|
|
1058
|
+
applyProjectContextOptions(links.command('list')
|
|
1059
|
+
.description('List project links from .aops/aops.config.json')).action(async (options) => {
|
|
1060
|
+
await runProjectLinksList(options);
|
|
1061
|
+
});
|
|
1062
|
+
cmd.addHelpText('after', buildOperatorCookbook({
|
|
1063
|
+
examples: [
|
|
1064
|
+
'aops-cli project list --json',
|
|
1065
|
+
'aops-cli project get --id <project-id> --json',
|
|
1066
|
+
'aops-cli project create --name "Demo Project" --apply --json',
|
|
1067
|
+
'aops-cli project update --id <project-id> --project-type app --apply --json',
|
|
1068
|
+
'aops-cli project link --slug aops-cockpit --mode hosted-only --apply --json',
|
|
1069
|
+
'aops-cli project migrate-local-root --project-slug aops --local-root .aops/projects/aops --dry-run --json',
|
|
1070
|
+
'aops-cli project links list --json',
|
|
1071
|
+
],
|
|
1072
|
+
guide: GUIDE_PATHS.agentspace,
|
|
1073
|
+
notes: [
|
|
1074
|
+
'Project list/get are read-only. Create/update/delete follow the standard hosted write guards.',
|
|
1075
|
+
'project link mutates only the repo project registry after verifying the hosted project exists and is not archived/deleted.',
|
|
1076
|
+
'project migrate-local-root is repo-local; run --dry-run first and require --apply --confirm for the real move.',
|
|
1077
|
+
],
|
|
1078
|
+
}));
|
|
1079
|
+
return cmd;
|
|
1080
|
+
}
|