@aopslabs/aops 0.3.6 → 0.3.8

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.
Files changed (37) hide show
  1. package/LICENSE +54 -14
  2. package/NOTICE +3 -3
  3. package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/discuss/SKILL.md +4 -4
  4. package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/user-guides/aops-cli.md +1 -1
  5. package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/manifest.json +7 -7
  6. package/agent-assets-release/agent-assets/inventory.json +5 -5
  7. package/agent-assets-release/agent-assets/projection.json +1 -1
  8. package/agent-assets-release/agent-assets-release.json +39 -39
  9. package/agent-assets-release/agent-assets-release.sigstore.json +23 -25
  10. package/assets/agent-assets/core/references/discuss/SKILL.md +4 -4
  11. package/assets/agent-assets/core/user-guides/aops-cli.md +1 -1
  12. package/dist/commands/discuss.js +95 -41
  13. package/dist/commands/doc.js +223 -45
  14. package/dist/lib/tui-launcher.js +19 -2
  15. package/native/tui/darwin-arm64/aops-tui +0 -0
  16. package/native/tui/win32-x64/aops-tui.exe +0 -0
  17. package/package.json +11 -10
  18. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/SKILL.md +0 -0
  19. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/agentspace/SKILL.md +0 -0
  20. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/aops-cli-core/SKILL.md +0 -0
  21. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/chatv3/SKILL.md +0 -0
  22. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/collaborative-work/SKILL.md +0 -0
  23. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/docman/SKILL.md +0 -0
  24. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/hosted-chat/SKILL.md +0 -0
  25. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/mission/SKILL.md +0 -0
  26. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/projectman/SKILL.md +0 -0
  27. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/sys/SKILL.md +0 -0
  28. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/view/SKILL.md +0 -0
  29. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/references/working-disciplines/SKILL.md +0 -0
  30. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/user-guides/agent-assets.md +0 -0
  31. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/user-guides/agentspace.md +0 -0
  32. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/user-guides/aops-system.md +0 -0
  33. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/user-guides/chatv3.md +0 -0
  34. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/user-guides/docman.md +0 -0
  35. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/user-guides/projectman.md +0 -0
  36. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/user-guides/sys.md +0 -0
  37. /package/agent-assets-release/agent-assets/core/{8b06775d96e37f8aef535673b8b855c11f29042f3c70ec4f273b6df04dbe824a → 0ffc854a8e32500f3d82c10a1e1ee6376d57e616201a323638f0f9caf8fb2706}/files/user-guides/working-disciplines.md +0 -0
@@ -1,6 +1,6 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
- import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
3
+ import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { Command } from 'commander';
6
6
  import { logError, logInfo, logSuccess } from '@aopslabs/xf-cli-ui';
@@ -887,14 +887,39 @@ function recordVersion(record) {
887
887
  const parsed = Number(record.version);
888
888
  return Number.isFinite(parsed) ? Math.trunc(parsed) : 0;
889
889
  }
890
- function selectLatestDocumentVersion(versions) {
891
- const candidates = versions.filter((version) => recordId(version, 'documentVersionId'));
892
- const currentVersion = candidates.find((version) => version.isCurrent === true);
893
- if (currentVersion)
894
- return currentVersion;
895
- return candidates
896
- .slice()
897
- .sort((left, right) => recordVersion(right) - recordVersion(left))[0];
890
+ export function selectCurrentPublishedDocumentVersion(versions, documentId) {
891
+ const normalizedDocumentId = normalizeNonEmpty(documentId);
892
+ if (!normalizedDocumentId)
893
+ throw new Error('Mirror version selection requires a document id.');
894
+ if (versions.length === 0) {
895
+ throw new Error(`Docman document ${normalizedDocumentId} has no version rows; mirror pull stopped before writing files.`);
896
+ }
897
+ const seenVersionIds = new Set();
898
+ for (const version of versions) {
899
+ const rowDocumentId = normalizeNonEmpty(version.documentId);
900
+ if (rowDocumentId !== normalizedDocumentId) {
901
+ throw new Error(`Docman version list returned a foreign row for document ${normalizedDocumentId}: ` +
902
+ `row ${recordId(version, 'documentVersionId') ?? '<missing-id>'} belongs to ${rowDocumentId ?? '<missing-document-id>'}.`);
903
+ }
904
+ const versionId = recordId(version, 'documentVersionId');
905
+ if (!versionId) {
906
+ throw new Error(`Docman document ${normalizedDocumentId} returned a version row without an id.`);
907
+ }
908
+ if (seenVersionIds.has(versionId)) {
909
+ throw new Error(`Docman document ${normalizedDocumentId} returned duplicate version id ${versionId}.`);
910
+ }
911
+ seenVersionIds.add(versionId);
912
+ }
913
+ const current = versions.filter((version) => version.isCurrent === true);
914
+ if (current.length !== 1) {
915
+ throw new Error(`Docman document ${normalizedDocumentId} must have exactly one current version; received ${current.length}. ` +
916
+ 'Mirror pull does not fall back to the highest version.');
917
+ }
918
+ const selected = current[0];
919
+ if (normalizeNonEmpty(selected.status)?.toLowerCase() !== 'published') {
920
+ throw new Error(`Docman document ${normalizedDocumentId} current version ${recordId(selected, 'documentVersionId') ?? '<missing-id>'} is not published.`);
921
+ }
922
+ return selected;
898
923
  }
899
924
  function findVersionBySourceHash(versions, sourceHash) {
900
925
  const shortHash = sourceHash.slice(0, 12);
@@ -1521,7 +1546,7 @@ function buildGroupPath(document, groupsById, groupsByUid) {
1521
1546
  }
1522
1547
  return path.join(...(segments.length > 0 ? segments : ['ungrouped']));
1523
1548
  }
1524
- async function writeDocMirrorFile(params) {
1549
+ function buildDocMirrorFile(params) {
1525
1550
  const documentSlug = normalizeNonEmpty(params.document.slug) ??
1526
1551
  pathSegmentSlug(normalizeNonEmpty(params.document.title), normalizeNonEmpty(params.document.id) ?? 'document');
1527
1552
  const filePath = path.join(params.outputRoot, params.groupPath, `${pathSegmentSlug(documentSlug, 'document')}.md`);
@@ -1544,11 +1569,12 @@ async function writeDocMirrorFile(params) {
1544
1569
  target: params.target,
1545
1570
  pulledAt: new Date().toISOString(),
1546
1571
  });
1547
- await mkdir(path.dirname(filePath), { recursive: true });
1548
- await writeFile(filePath, `${frontmatter}<!-- READ-ONLY MIRROR: update Docman/source docs, then run aops-cli doc mirror pull. -->\n\n${params.content}`, 'utf8');
1549
- return filePath;
1572
+ return {
1573
+ filePath,
1574
+ content: `${frontmatter}<!-- READ-ONLY MIRROR: update Docman/source docs, then run aops-cli doc mirror pull. -->\n\n${params.content}`,
1575
+ };
1550
1576
  }
1551
- async function writeDocMirrorIndex(params) {
1577
+ function buildDocMirrorIndex(params) {
1552
1578
  const indexPath = path.join(params.outputRoot, 'index.md');
1553
1579
  const frontmatter = renderFrontmatter({
1554
1580
  schemaVersion: 1,
@@ -1575,9 +1601,79 @@ async function writeDocMirrorIndex(params) {
1575
1601
  }),
1576
1602
  '',
1577
1603
  ];
1578
- await mkdir(params.outputRoot, { recursive: true });
1579
- await writeFile(indexPath, lines.join('\n'), 'utf8');
1580
- return indexPath;
1604
+ return { filePath: indexPath, content: lines.join('\n') };
1605
+ }
1606
+ export async function stageMirrorFile(targetPath, content, sequence) {
1607
+ await mkdir(path.dirname(targetPath), { recursive: true });
1608
+ const temporaryPath = `${targetPath}.aops-stage-${process.pid}-${sequence}-${randomUUID()}`;
1609
+ await writeFile(temporaryPath, content, 'utf8');
1610
+ return { targetPath, temporaryPath };
1611
+ }
1612
+ function isFileSystemError(error, code) {
1613
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === code);
1614
+ }
1615
+ async function replaceStagedMirrorFile(file) {
1616
+ const backupPath = `${file.temporaryPath}.previous`;
1617
+ let movedExistingTarget = false;
1618
+ try {
1619
+ try {
1620
+ await rename(file.targetPath, backupPath);
1621
+ movedExistingTarget = true;
1622
+ }
1623
+ catch (error) {
1624
+ if (!isFileSystemError(error, 'ENOENT'))
1625
+ throw error;
1626
+ }
1627
+ try {
1628
+ await rename(file.temporaryPath, file.targetPath);
1629
+ }
1630
+ catch (error) {
1631
+ if (movedExistingTarget) {
1632
+ try {
1633
+ await rename(backupPath, file.targetPath);
1634
+ }
1635
+ catch (restoreError) {
1636
+ throw new AggregateError([error, restoreError], `Docman mirror replacement failed and could not restore ${file.targetPath}.`);
1637
+ }
1638
+ }
1639
+ throw error;
1640
+ }
1641
+ return { ...file, backupPath, targetExisted: movedExistingTarget };
1642
+ }
1643
+ catch (error) {
1644
+ await unlink(file.temporaryPath).catch(() => undefined);
1645
+ throw error;
1646
+ }
1647
+ }
1648
+ export async function commitStagedMirrorFiles(files) {
1649
+ const committed = [];
1650
+ try {
1651
+ for (const file of files) {
1652
+ committed.push(await replaceStagedMirrorFile(file));
1653
+ }
1654
+ }
1655
+ catch (error) {
1656
+ const rollbackErrors = [];
1657
+ for (const file of [...committed].reverse()) {
1658
+ try {
1659
+ await unlink(file.targetPath).catch((unlinkError) => {
1660
+ if (!isFileSystemError(unlinkError, 'ENOENT'))
1661
+ throw unlinkError;
1662
+ });
1663
+ if (file.targetExisted)
1664
+ await rename(file.backupPath, file.targetPath);
1665
+ }
1666
+ catch (rollbackError) {
1667
+ rollbackErrors.push(rollbackError);
1668
+ }
1669
+ }
1670
+ await Promise.all(files.map((file) => unlink(file.temporaryPath).catch(() => undefined)));
1671
+ if (rollbackErrors.length > 0) {
1672
+ throw new AggregateError([error, ...rollbackErrors], 'Docman mirror commit failed and rollback was incomplete.');
1673
+ }
1674
+ throw error;
1675
+ }
1676
+ await Promise.all(committed.filter((file) => file.targetExisted).map((file) => unlink(file.backupPath).catch(() => undefined)));
1581
1677
  }
1582
1678
  async function listDocMirrorIndexEntries(outputRoot) {
1583
1679
  const files = await listLocalMirrorMarkdownFiles(outputRoot);
@@ -2120,6 +2216,11 @@ function normalizeDocImportSlugStrategy(options) {
2120
2216
  return explicit;
2121
2217
  throw new Error('Invalid --slug-strategy. Expected one of: hash-suffix-on-collision, kebab-from-title.');
2122
2218
  }
2219
+ export function resolveDocImportToolId(dryRun) {
2220
+ return dryRun
2221
+ ? 'docman.document-version.import-headings.plan'
2222
+ : 'docman.document-version.import-headings';
2223
+ }
2123
2224
  export async function runDocImportMarkdown(options = {}) {
2124
2225
  try {
2125
2226
  if (options.fromMarkdown !== true) {
@@ -2143,6 +2244,10 @@ export async function runDocImportMarkdown(options = {}) {
2143
2244
  })
2144
2245
  : undefined;
2145
2246
  const dryRun = options.dryRun === true || options.preview === true || options.apply !== true;
2247
+ const planToken = normalizeNonEmpty(options.planToken);
2248
+ if (dryRun && planToken) {
2249
+ throw new Error('--plan-token is only valid with --apply.');
2250
+ }
2146
2251
  const auditFields = await resolveDocAuditFields(apiState, options, {}, 'create');
2147
2252
  const importOptions = compactPayload({
2148
2253
  dryRun,
@@ -2167,13 +2272,14 @@ export async function runDocImportMarkdown(options = {}) {
2167
2272
  nodes,
2168
2273
  },
2169
2274
  options: importOptions,
2275
+ ...(!dryRun && planToken ? { planToken } : {}),
2170
2276
  ...auditFields,
2171
2277
  });
2172
2278
  if (baselineGuard?.status === 'blocked' && !dryRun && options.confirm !== true) {
2173
2279
  throw new Error(`Doc import baseline guard blocked ${baselineGuard.blockingDeltaCount} unrelated markdown deltas. ` +
2174
2280
  'Run the command with --dry-run --json to inspect result.baselineGuard, add --guard-target for the intended heading, or use --confirm only after reviewing the full-import delta.');
2175
2281
  }
2176
- const toolId = 'docman.document-version.import-headings';
2282
+ const toolId = resolveDocImportToolId(dryRun);
2177
2283
  const invokeInput = compactPayload({
2178
2284
  pathParams: { id: documentVersionId },
2179
2285
  body: input,
@@ -2407,6 +2513,36 @@ export async function runDocMirrorPull(options = {}) {
2407
2513
  }), compactPayload({
2408
2514
  limit: toInteger(options.limit, '--limit'),
2409
2515
  }));
2516
+ const expectedGroup = groupUid ? groupsByUid.get(groupUid) : undefined;
2517
+ const expectedGroupId = normalizeNonEmpty(expectedGroup?.id);
2518
+ const expectedStatus = normalizeNonEmpty(options.status)?.toLowerCase();
2519
+ const expectedScopeId = normalizeNonEmpty(resolvedContext.scopeId);
2520
+ const seenDocumentIds = new Set();
2521
+ for (const document of allDocuments) {
2522
+ const documentId = recordId(document, 'documentId');
2523
+ if (!documentId) {
2524
+ throw new Error('Docman document list returned a row without a document id; mirror pull stopped before writing files.');
2525
+ }
2526
+ if (seenDocumentIds.has(documentId)) {
2527
+ throw new Error(`Docman document list returned duplicate document id ${documentId}.`);
2528
+ }
2529
+ seenDocumentIds.add(documentId);
2530
+ const rowScopeId = normalizeNonEmpty(document.scopeId);
2531
+ if (expectedScopeId && rowScopeId && rowScopeId !== expectedScopeId) {
2532
+ throw new Error(`Docman document list returned foreign-scope row ${documentId} (${rowScopeId}, expected ${expectedScopeId}).`);
2533
+ }
2534
+ if (groupUid) {
2535
+ const rowGroupUid = normalizeNonEmpty(document.groupUid);
2536
+ const rowGroupId = normalizeNonEmpty(document.groupId);
2537
+ if (rowGroupUid !== groupUid && (!expectedGroupId || rowGroupId !== expectedGroupId)) {
2538
+ throw new Error(`Docman document list returned foreign-group row ${documentId}; expected group ${groupUid}.`);
2539
+ }
2540
+ }
2541
+ const rowStatus = normalizeNonEmpty(document.status)?.toLowerCase();
2542
+ if (expectedStatus && rowStatus !== expectedStatus) {
2543
+ throw new Error(`Docman document list returned foreign-status row ${documentId}; expected ${expectedStatus}, received ${rowStatus ?? '<missing>'}.`);
2544
+ }
2545
+ }
2410
2546
  const documents = documentSlugFilters.size > 0
2411
2547
  ? allDocuments.filter((document) => {
2412
2548
  const documentSlug = normalizeSlugFilter(document.slug);
@@ -2419,22 +2555,30 @@ export async function runDocMirrorPull(options = {}) {
2419
2555
  throw new Error(`No Docman documents matched --document-slug ${[...documentSlugFilters].join(', ')}.`);
2420
2556
  }
2421
2557
  const plans = [];
2558
+ const seenVersionIds = new Set();
2559
+ const seenMirrorPaths = new Set();
2422
2560
  for (const document of documents) {
2423
2561
  const documentId = recordId(document, 'documentId');
2424
- if (!documentId) {
2425
- plans.push({ title: document.title, skipped: true, reason: 'missing-document-id' });
2426
- continue;
2427
- }
2562
+ if (!documentId)
2563
+ throw new Error('Docman document row lost its validated id unexpectedly.');
2428
2564
  const versions = await listDocumentVersions(apiState, options, resolvedContext, documentId);
2429
- const latest = selectLatestDocumentVersion(versions);
2565
+ const latest = selectCurrentPublishedDocumentVersion(versions, documentId);
2430
2566
  const documentVersionId = recordId(latest, 'documentVersionId');
2431
- if (!latest || !documentVersionId) {
2432
- plans.push({ documentId, title: document.title, skipped: true, reason: 'missing-document-version' });
2433
- continue;
2567
+ if (!documentVersionId)
2568
+ throw new Error(`Docman document ${documentId} current published version is missing its id.`);
2569
+ if (seenVersionIds.has(documentVersionId)) {
2570
+ throw new Error(`Docman mirror plan contains duplicate version id ${documentVersionId}.`);
2434
2571
  }
2572
+ seenVersionIds.add(documentVersionId);
2435
2573
  const groupPath = buildGroupPath(document, groupsById, groupsByUid);
2436
2574
  const documentSlug = normalizeNonEmpty(document.slug) ??
2437
2575
  pathSegmentSlug(normalizeNonEmpty(document.title), normalizeNonEmpty(documentId) ?? 'document');
2576
+ const mirrorPath = path.join(groupPath, `${pathSegmentSlug(documentSlug, 'document')}.md`);
2577
+ const canonicalMirrorPath = path.resolve(outputRoot, mirrorPath).toLowerCase();
2578
+ if (seenMirrorPaths.has(canonicalMirrorPath)) {
2579
+ throw new Error(`Docman mirror plan contains duplicate output path ${mirrorPath}.`);
2580
+ }
2581
+ seenMirrorPaths.add(canonicalMirrorPath);
2438
2582
  plans.push({
2439
2583
  documentId,
2440
2584
  documentVersionId,
@@ -2442,7 +2586,7 @@ export async function runDocMirrorPull(options = {}) {
2442
2586
  title: document.title,
2443
2587
  slug: documentSlug,
2444
2588
  groupPath,
2445
- mirrorPath: path.join(groupPath, `${pathSegmentSlug(documentSlug, 'document')}.md`),
2589
+ mirrorPath,
2446
2590
  });
2447
2591
  }
2448
2592
  const input = compactPayload({
@@ -2462,16 +2606,16 @@ export async function runDocMirrorPull(options = {}) {
2462
2606
  input,
2463
2607
  }))
2464
2608
  return;
2465
- const written = [];
2609
+ const materializedPlans = [];
2466
2610
  for (const plan of plans) {
2467
2611
  const documentId = normalizeNonEmpty(plan.documentId);
2468
2612
  const documentVersionId = normalizeNonEmpty(plan.documentVersionId);
2469
- if (!documentId || !documentVersionId) {
2470
- written.push(plan);
2471
- continue;
2472
- }
2473
- const document = documents.find((entry) => recordId(entry, 'documentId') === documentId) ?? {};
2474
- const latest = { id: documentVersionId, version: plan.documentVersion };
2613
+ if (!documentId || !documentVersionId)
2614
+ throw new Error('Validated mirror plan lost a document or version id.');
2615
+ const document = documents.find((entry) => recordId(entry, 'documentId') === documentId);
2616
+ if (!document)
2617
+ throw new Error(`Validated mirror plan references unknown document ${documentId}.`);
2618
+ const documentVersion = { id: documentVersionId, documentId, version: plan.documentVersion, status: 'published', isCurrent: true };
2475
2619
  const materialized = await callDocmanVersionRoute(apiState, options, resolvedContext, {
2476
2620
  documentVersionId,
2477
2621
  suffix: 'materialize',
@@ -2483,30 +2627,62 @@ export async function runDocMirrorPull(options = {}) {
2483
2627
  fallbackLocale: normalizeNonEmpty(options.fallbackLocale),
2484
2628
  }),
2485
2629
  });
2630
+ const materializedPayload = unwrapResultData(materialized);
2631
+ const returnedVersionId = normalizeNonEmpty(materializedPayload?.documentVersionId);
2632
+ if (returnedVersionId !== documentVersionId) {
2633
+ throw new Error(`Docman materialize returned version ${returnedVersionId ?? '<missing>'}; expected ${documentVersionId}. ` +
2634
+ 'Mirror pull stopped before replacing any destination file.');
2635
+ }
2486
2636
  const content = materializedContent(materialized);
2487
- const outputPath = await writeDocMirrorFile({
2637
+ materializedPlans.push({ plan, document, documentVersion, content });
2638
+ }
2639
+ const existingIndexEntries = await listDocMirrorIndexEntries(outputRoot);
2640
+ const written = [];
2641
+ const artifacts = materializedPlans.map(({ plan, document, documentVersion, content }) => {
2642
+ const artifact = buildDocMirrorFile({
2488
2643
  outputRoot,
2489
2644
  groupPath: normalizeNonEmpty(plan.groupPath) ?? 'ungrouped',
2490
2645
  document,
2491
- documentVersion: latest,
2646
+ documentVersion,
2492
2647
  resolvedContext,
2493
2648
  target,
2494
2649
  content,
2495
2650
  });
2496
2651
  written.push({
2497
2652
  ...plan,
2498
- outputPath,
2653
+ outputPath: artifact.filePath,
2499
2654
  });
2655
+ return artifact;
2656
+ });
2657
+ const indexEntriesByPath = new Map();
2658
+ for (const entry of existingIndexEntries) {
2659
+ const mirrorPath = normalizeNonEmpty(entry.mirrorPath);
2660
+ if (mirrorPath)
2661
+ indexEntriesByPath.set(mirrorPath.toLowerCase(), entry);
2500
2662
  }
2501
- const localIndexEntries = await listDocMirrorIndexEntries(outputRoot);
2502
- const indexEntries = localIndexEntries.length > 0
2503
- ? localIndexEntries
2504
- : written.filter((entry) => normalizeNonEmpty(entry.outputPath));
2505
- const indexPath = await writeDocMirrorIndex({
2663
+ for (const entry of written) {
2664
+ const mirrorPath = normalizeNonEmpty(entry.mirrorPath);
2665
+ if (mirrorPath)
2666
+ indexEntriesByPath.set(mirrorPath.toLowerCase(), entry);
2667
+ }
2668
+ const indexArtifact = buildDocMirrorIndex({
2506
2669
  outputRoot,
2507
2670
  resolvedContext,
2508
- entries: indexEntries,
2671
+ entries: [...indexEntriesByPath.values()],
2509
2672
  });
2673
+ const staged = [];
2674
+ try {
2675
+ for (const [index, artifact] of artifacts.entries()) {
2676
+ staged.push(await stageMirrorFile(artifact.filePath, artifact.content, index));
2677
+ }
2678
+ staged.push(await stageMirrorFile(indexArtifact.filePath, indexArtifact.content, artifacts.length));
2679
+ }
2680
+ catch (error) {
2681
+ await Promise.all(staged.map((file) => unlink(file.temporaryPath).catch(() => undefined)));
2682
+ throw error;
2683
+ }
2684
+ await commitStagedMirrorFiles(staged);
2685
+ const indexPath = indexArtifact.filePath;
2510
2686
  await emitDocResult({
2511
2687
  options,
2512
2688
  command: 'doc.mirror.pull',
@@ -2517,7 +2693,7 @@ export async function runDocMirrorPull(options = {}) {
2517
2693
  outputRoot,
2518
2694
  indexPath,
2519
2695
  writtenCount: written.filter((entry) => normalizeNonEmpty(entry.outputPath)).length,
2520
- skippedCount: written.filter((entry) => Boolean(entry.skipped)).length,
2696
+ skippedCount: 0,
2521
2697
  documents: written,
2522
2698
  },
2523
2699
  artifacts: {
@@ -5097,6 +5273,7 @@ export function makeDocCommand() {
5097
5273
  .option('--baseline <path>', 'Prior materialized markdown path used to guard against unrelated body loss')
5098
5274
  .option('--guard-target <heading>', 'Heading path/title allowed to change when --baseline is provided (repeatable)', collectRepeatedOption, [])
5099
5275
  .option('--dry-run', 'Execute service-side import planning without mutating rows')
5276
+ .option('--plan-token <token>', 'Apply only if this server-issued import plan is still current')
5100
5277
  .option('--existing-graph-policy <policy>', 'Existing graph policy: error, append, replace', 'error')
5101
5278
  .option('--append-existing-graph', 'Append into an existing graph explicitly')
5102
5279
  .option('--replace-existing-graph', 'Request explicit replace semantics')
@@ -5117,6 +5294,7 @@ export function makeDocCommand() {
5117
5294
  'In JSON, warning codes are under result.summary.warnings[].code, or result.import.summary.warnings[].code when baselineGuard wraps the import result.',
5118
5295
  'Use --synthesize-overview-pages when direct H2/H3 body text must become child Overview pages, then verify the mirror after publish/pull.',
5119
5296
  'Use --baseline and --guard-target for full-document imports; apply is blocked on unrelated deltas unless --confirm is explicit.',
5297
+ 'A dry-run returns a short-lived planToken. Pass it to --apply --plan-token <token> to reject source or graph drift; apply without a token remains supported.',
5120
5298
  ],
5121
5299
  }));
5122
5300
  const mirror = cmd.command('mirror').description('Docman mirror pull and markdown import helpers');
@@ -1,11 +1,21 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
2
+ import { accessSync, constants, existsSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  const SUPPORTED_TUI_TARGETS = new Set([
6
6
  'darwin-arm64',
7
7
  'win32-x64',
8
8
  ]);
9
+ const TUI_REINSTALL_MESSAGE = 'The bundled AOPS TUI is not executable. Reinstall @aopslabs/aops or use aops --help.';
10
+ function isExecutableFile(filePath) {
11
+ try {
12
+ accessSync(filePath, constants.X_OK);
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
9
19
  function packageRootFromModule() {
10
20
  return fileURLToPath(new URL('../../', import.meta.url));
11
21
  }
@@ -41,10 +51,17 @@ export async function launchBundledTui(options = {}) {
41
51
  if (!fileExists(invocation.command)) {
42
52
  throw new Error('The bundled AOPS TUI is missing. Reinstall @aopslabs/aops or use aops --help.');
43
53
  }
54
+ const platform = options.platform ?? process.platform;
55
+ const fileExecutable = options.fileExecutable ?? isExecutableFile;
56
+ if (platform !== 'win32' && !fileExecutable(invocation.command)) {
57
+ throw new Error(TUI_REINSTALL_MESSAGE);
58
+ }
44
59
  const spawnProcess = options.spawnProcess ?? spawn;
45
60
  const child = spawnProcess(invocation.command, [...invocation.args], invocation.options);
46
61
  return await new Promise((resolve, reject) => {
47
- child.once('error', reject);
62
+ child.once('error', (error) => {
63
+ reject(error.code === 'EACCES' ? new Error(TUI_REINSTALL_MESSAGE) : error);
64
+ });
48
65
  child.once('exit', (code, signal) => {
49
66
  if (signal) {
50
67
  reject(new Error(`The AOPS TUI stopped after receiving ${signal}.`));
Binary file
Binary file
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@aopslabs/aops",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "AOPS CLI and terminal setup application.",
6
- "aopsDockerServerVersion": "0.2.8",
6
+ "aopsDockerServerVersion": "0.2.9",
7
7
  "license": "SEE LICENSE IN LICENSE",
8
8
  "repository": {
9
9
  "type": "git",
@@ -39,7 +39,7 @@
39
39
  "test:commercial-license": "node --test test/commercial-license.test.mjs",
40
40
  "test:commercial-runbook": "node --test test/commercial-runbook.test.mjs",
41
41
  "test:commercial-performance": "node --test test/commercial-performance.test.mjs",
42
- "test:package-identity": "node --test test/package-identity.test.mjs",
42
+ "test:package-identity": "node --test test/package-identity.test.mjs test/npm-tar-entry-mode.test.mjs",
43
43
  "test:tui-launcher": "node --test test/tui-launcher.test.mjs",
44
44
  "test:skill-discovery": "node --test test/skill-discovery.test.mjs",
45
45
  "test:target-auth": "node --test test/target-auth.test.mjs test/device-login.test.mjs",
@@ -48,24 +48,25 @@
48
48
  "test:r6-auth-cutover": "node --test test/r6-auth-cutover.test.mjs",
49
49
  "test:container-runtime": "node --test test/container-runtime.test.mjs test/community-native-postgres.test.mjs",
50
50
  "test:kernel-alignment": "node --test test/kernel-alignment.test.mjs",
51
+ "test:hosted-contract-hardening": "node --test test/hosted-contract-hardening.test.mjs",
51
52
  "test:home": "node --test test/home-menu.test.mjs",
52
53
  "test:launchers": "node --test test/terminal-launchers.test.mjs",
53
- "test:shortcuts": "pnpm run test:home && pnpm run test:cockpit && pnpm run test:launchers && pnpm run test:assets-menu && pnpm run test:agent-assets-content && pnpm run test:setup-install && pnpm run test:container-runtime && pnpm run test:native-lifecycle && pnpm run test:operation-journal && pnpm run test:commercial-canonical-order && pnpm run test:commercial-runtime-closure && pnpm run test:commercial-artifact && pnpm run test:commercial-final-readiness && pnpm run test:commercial-admission && pnpm run test:commercial-license && pnpm run test:commercial-runbook && pnpm run test:package-identity && pnpm run test:skill-discovery && pnpm run test:target-transport && pnpm run test:target-auth && pnpm run test:r6-auth-cutover && pnpm run test:r6-cutover-baseline",
54
+ "test:shortcuts": "pnpm run test:home && pnpm run test:cockpit && pnpm run test:launchers && pnpm run test:assets-menu && pnpm run test:agent-assets-content && pnpm run test:setup-install && pnpm run test:container-runtime && pnpm run test:native-lifecycle && pnpm run test:operation-journal && pnpm run test:commercial-canonical-order && pnpm run test:commercial-runtime-closure && pnpm run test:commercial-artifact && pnpm run test:commercial-final-readiness && pnpm run test:commercial-admission && pnpm run test:commercial-license && pnpm run test:commercial-runbook && pnpm run test:package-identity && pnpm run test:skill-discovery && pnpm run test:target-transport && pnpm run test:target-auth && pnpm run test:r6-auth-cutover && pnpm run test:r6-cutover-baseline && pnpm run test:hosted-contract-hardening",
54
55
  "typecheck": "tsc -p tsconfig.json --noEmit",
55
56
  "start": "node dist/main.js"
56
57
  },
57
58
  "dependencies": {
58
- "@aopslabs/aops-server": "0.2.8",
59
- "@aopslabs/artifact-trust-contracts": "0.3.0",
59
+ "@aopslabs/aops-server": "0.2.9",
60
+ "@aopslabs/artifact-trust-contracts": "0.3.1",
60
61
  "@aopslabs/cli-kit": "0.1.0",
61
- "@aopslabs/aops-host-registration": "0.2.0",
62
- "@aopslabs/aops-pg-bootstrap": "0.2.1",
63
- "@aopslabs/aops-runtime-config": "0.2.0",
62
+ "@aopslabs/aops-host-registration": "0.2.1",
63
+ "@aopslabs/aops-pg-bootstrap": "0.2.2",
64
+ "@aopslabs/aops-runtime-config": "0.2.1",
64
65
  "@sigstore/bundle": "4.0.0",
65
66
  "@sigstore/protobuf-specs": "0.5.1",
66
67
  "@sigstore/tuf": "4.0.2",
67
68
  "@sigstore/verify": "3.1.1",
68
- "@aopslabs/api-client": "0.2.0",
69
+ "@aopslabs/api-client": "0.2.1",
69
70
  "@aopslabs/domain-product-client-chatv3": "0.2.2",
70
71
  "@aopslabs/xf-cli-ui": "0.2.0",
71
72
  "commander": "14.0.3",