@lesliechan721/aw 0.1.0-beta.4 → 0.1.0-beta.6

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 (99) hide show
  1. package/dist/authoring/scripts/lib/plugin-version-check.d.mts +14 -0
  2. package/dist/authoring/scripts/lib/plugin-version-check.mjs +10 -224
  3. package/dist/authoring/scripts/lib/plugin-version-check.mjs.map +1 -1
  4. package/dist/authoring/scripts/remove-plugin.d.mts +2 -0
  5. package/dist/authoring/scripts/remove-plugin.mjs +366 -0
  6. package/dist/authoring/scripts/remove-plugin.mjs.map +1 -0
  7. package/dist/authoring/scripts/validate-plugin.mjs +9 -46
  8. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  9. package/dist/scripts/bundle-plugins.d.ts +0 -2
  10. package/dist/scripts/bundle-plugins.js +4 -2
  11. package/dist/scripts/bundle-plugins.js.map +1 -1
  12. package/dist/scripts/plugin-staging.d.ts +1 -16
  13. package/dist/scripts/plugin-staging.js +8 -115
  14. package/dist/scripts/plugin-staging.js.map +1 -1
  15. package/dist/src/authoring/workspace-commands.d.ts +9 -1
  16. package/dist/src/authoring/workspace-commands.js +24 -16
  17. package/dist/src/authoring/workspace-commands.js.map +1 -1
  18. package/dist/src/commands/catalog-publication-inspection.d.ts +106 -0
  19. package/dist/src/commands/catalog-publication-inspection.js +444 -0
  20. package/dist/src/commands/catalog-publication-inspection.js.map +1 -0
  21. package/dist/src/commands/catalog-publish.d.ts +18 -2
  22. package/dist/src/commands/catalog-publish.js +261 -316
  23. package/dist/src/commands/catalog-publish.js.map +1 -1
  24. package/dist/src/commands/catalog-status.d.ts +60 -0
  25. package/dist/src/commands/catalog-status.js +251 -0
  26. package/dist/src/commands/catalog-status.js.map +1 -0
  27. package/dist/src/commands/plugin-capabilities.d.ts +22 -0
  28. package/dist/src/commands/plugin-capabilities.js +143 -0
  29. package/dist/src/commands/plugin-capabilities.js.map +1 -0
  30. package/dist/src/commands/rebuild-command.d.ts +2 -0
  31. package/dist/src/commands/rebuild-command.js +37 -6
  32. package/dist/src/commands/rebuild-command.js.map +1 -1
  33. package/dist/src/commands/rebuild-integration.d.ts +1 -0
  34. package/dist/src/commands/rebuild-integration.js +55 -57
  35. package/dist/src/commands/rebuild-integration.js.map +1 -1
  36. package/dist/src/commands/status-v4.d.ts +21 -0
  37. package/dist/src/commands/status-v4.js +63 -2
  38. package/dist/src/commands/status-v4.js.map +1 -1
  39. package/dist/src/index.js +79 -18
  40. package/dist/src/index.js.map +1 -1
  41. package/dist/src/tui/components.d.ts +70 -0
  42. package/dist/src/tui/components.js +149 -6
  43. package/dist/src/tui/components.js.map +1 -1
  44. package/dist/src/tui/console-app-v4.js +1660 -628
  45. package/dist/src/tui/console-app-v4.js.map +1 -1
  46. package/dist/src/tui/controller-runtime.d.ts +5 -1
  47. package/dist/src/tui/controller-runtime.js +184 -33
  48. package/dist/src/tui/controller-runtime.js.map +1 -1
  49. package/dist/src/tui/controller.d.ts +35 -2
  50. package/dist/src/tui/controller.js +300 -39
  51. package/dist/src/tui/controller.js.map +1 -1
  52. package/dist/src/tui/diagnostics.d.ts +31 -0
  53. package/dist/src/tui/diagnostics.js +71 -0
  54. package/dist/src/tui/diagnostics.js.map +1 -0
  55. package/dist/src/tui/index.d.ts +5 -3
  56. package/dist/src/tui/index.js +3 -1
  57. package/dist/src/tui/index.js.map +1 -1
  58. package/dist/src/tui/model.d.ts +33 -4
  59. package/dist/src/tui/model.js +252 -17
  60. package/dist/src/tui/model.js.map +1 -1
  61. package/dist/src/tui/router.d.ts +15 -9
  62. package/dist/src/tui/router.js +35 -0
  63. package/dist/src/tui/router.js.map +1 -1
  64. package/dist/src/tui/theme.d.ts +18 -10
  65. package/dist/src/tui/theme.js +18 -10
  66. package/dist/src/tui/theme.js.map +1 -1
  67. package/dist/src/tui/viewport.d.ts +20 -0
  68. package/dist/src/tui/viewport.js +29 -0
  69. package/dist/src/tui/viewport.js.map +1 -0
  70. package/dist/src/utils/catalog-runtime-projection.d.ts +1 -7
  71. package/dist/src/utils/catalog-runtime-projection.js +2 -62
  72. package/dist/src/utils/catalog-runtime-projection.js.map +1 -1
  73. package/dist/src/utils/catalog-source-cache.d.ts +12 -11
  74. package/dist/src/utils/catalog-source-cache.js +20 -0
  75. package/dist/src/utils/catalog-source-cache.js.map +1 -1
  76. package/dist/src/utils/dynamic-skill-tree.js +1 -1
  77. package/dist/src/utils/dynamic-skill-tree.js.map +1 -1
  78. package/dist/src/utils/git-catalog-provider.js +1 -1
  79. package/dist/src/utils/git-catalog-provider.js.map +1 -1
  80. package/dist/src/utils/manifest.d.ts +0 -1
  81. package/dist/src/utils/manifest.js +0 -1
  82. package/dist/src/utils/manifest.js.map +1 -1
  83. package/dist/src/utils/plugin-config.js +28 -30
  84. package/dist/src/utils/plugin-config.js.map +1 -1
  85. package/dist/src/utils/skill-validation.d.ts +1 -1
  86. package/dist/src/utils/skill-validation.js +1 -1
  87. package/dist/src/utils/skill-validation.js.map +1 -1
  88. package/dist/src/utils/state-paths.d.ts +1 -0
  89. package/dist/src/utils/state-paths.js +5 -0
  90. package/dist/src/utils/state-paths.js.map +1 -1
  91. package/package.json +3 -4
  92. package/dist/src/utils/plugin-authoring-capabilities.d.ts +0 -23
  93. package/dist/src/utils/plugin-authoring-capabilities.js +0 -37
  94. package/dist/src/utils/plugin-authoring-capabilities.js.map +0 -1
  95. package/dist/src/utils/plugin-authoring-capabilities.json +0 -92
  96. package/dist/src/utils/skill-frontmatter-capabilities.d.ts +0 -201
  97. package/dist/src/utils/skill-frontmatter-capabilities.js +0 -5
  98. package/dist/src/utils/skill-frontmatter-capabilities.js.map +0 -1
  99. package/dist/src/utils/skill-frontmatter-capabilities.json +0 -200
@@ -1,108 +1,36 @@
1
1
  import crypto from 'node:crypto';
2
- import { execFile } from 'node:child_process';
3
- import os from 'node:os';
4
2
  import path from 'node:path';
5
- import { promisify } from 'node:util';
6
3
  import fs from 'fs-extra';
7
- import { assertStrictSemVer, preparePublishedWorkspaceCatalog, readPluginWorkspace, validatePublishedCatalogSnapshot, } from '@lesliechan721/aw-plugin-core';
8
- import { bundleLocalPlugin, createCatalogRuntimeImportResolver } from '../../scripts/plugin-staging.js';
9
- import { CLI_RESERVED_IMPORTS } from '../utils/manifest.js';
10
- import { resolveCliPackageRoot } from '../utils/paths.js';
11
- const execFileAsync = promisify(execFile);
12
- const LEGACY_ATTEMPTED_MARKER = path.join('.aw', 'publish', 'attempted.json');
13
- const INHERITED_GIT_AUTH_ENV = new Set([
14
- 'GIT_ASKPASS',
15
- 'GIT_HTTP_USER_AGENT',
16
- 'GIT_SSH',
17
- 'GIT_SSH_COMMAND',
18
- 'GIT_SSH_VARIANT',
19
- 'GIT_SSL_CAINFO',
20
- 'GIT_SSL_NO_VERIFY',
21
- 'GIT_TERMINAL_PROMPT',
22
- ]);
4
+ import { validatePublishedCatalogSnapshot } from '@lesliechan721/aw-plugin-core';
5
+ import { disposeCatalogPublicationInspection, parseRemoteRefs, prepareCatalogPublicationInspection, publicationGit, publicationGitExit, } from './catalog-publication-inspection.js';
6
+ export { safeRemoteIdentity, sanitizedGitEnvironment } from './catalog-publication-inspection.js';
23
7
  function digest(value) {
24
8
  return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
25
9
  }
26
- export function sanitizedGitEnvironment(source) {
27
- return Object.fromEntries(Object.entries(source).filter(([key]) => !key.startsWith('GIT_') || INHERITED_GIT_AUTH_ENV.has(key)));
28
- }
29
- export function safeRemoteIdentity(value) {
30
- if (/^[a-z][a-z\d+.-]*:\/\//iu.test(value)) {
31
- try {
32
- const url = new URL(value);
33
- url.username = '';
34
- url.password = '';
35
- url.search = '';
36
- url.hash = '';
37
- return url.toString();
38
- }
39
- catch {
40
- // Fall through to conservative display sanitization for malformed URLs.
41
- }
10
+ function canonicalJson(value) {
11
+ if (Array.isArray(value))
12
+ return `[${value.map(canonicalJson).join(',')}]`;
13
+ if (value !== null && typeof value === 'object') {
14
+ const record = value;
15
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
42
16
  }
43
- const withoutQuery = value.split(/[?#]/u, 1)[0];
44
- return withoutQuery
45
- .replace(/^([^/@\s:]+):([^/@\s]+)@/u, '')
46
- .replace(/^[^@\s]+@(?=[^:/\s]+[/:])/u, '');
17
+ return JSON.stringify(value);
47
18
  }
48
- function attemptPath(workspaceRoot, catalogVersion) {
49
- return path.join(workspaceRoot, '.aw', 'publish', 'attempts', `${catalogVersion}.json`);
50
- }
51
- async function migrateLegacyAttempt(workspaceRoot, catalogVersion) {
52
- const legacyPath = path.join(workspaceRoot, LEGACY_ATTEMPTED_MARKER);
53
- if (!(await fs.pathExists(legacyPath)))
54
- return;
55
- let recordedVersion = catalogVersion;
56
- try {
57
- const legacy = await fs.readJson(legacyPath);
58
- if (legacy.schemaVersion === 1 && typeof legacy.catalogVersion === 'string') {
59
- recordedVersion = assertStrictSemVer(legacy.catalogVersion, 'legacy attempted Catalog version');
60
- }
61
- }
62
- catch {
63
- // Bind an unreadable legacy marker to the current version. A later version uses a new file.
64
- }
65
- const destination = attemptPath(workspaceRoot, recordedVersion);
66
- await fs.ensureDir(path.dirname(destination));
67
- if (await fs.pathExists(destination))
68
- return;
69
- await fs.rename(legacyPath, destination);
19
+ export function catalogPublicationDescriptorDigest(descriptor) {
20
+ return digest(canonicalJson(descriptor));
70
21
  }
71
- async function git(cwd, args, env, label) {
72
- try {
73
- const result = await execFileAsync('git', [...args], {
74
- cwd,
75
- env,
76
- encoding: 'utf8',
77
- maxBuffer: 16 * 1024 * 1024,
78
- });
79
- return result.stdout.trim();
80
- }
81
- catch (error) {
82
- throw new Error(label, { cause: error });
83
- }
84
- }
85
- async function gitExit(cwd, args, env) {
86
- try {
87
- await execFileAsync('git', [...args], { cwd, env, encoding: 'utf8' });
88
- return true;
89
- }
90
- catch {
91
- return false;
92
- }
93
- }
94
- function parseRemoteRefs(output) {
95
- return new Map(output.split('\n').filter(Boolean).map((line) => {
96
- const [objectId, ref] = line.split(/\s+/u);
97
- return [ref, objectId];
98
- }));
99
- }
100
- async function writeJsonAtomic(filePath, value) {
22
+ async function writeJsonAtomic(filePath, value, exclusive) {
101
23
  await fs.ensureDir(path.dirname(filePath));
102
24
  const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
103
25
  await fs.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
104
26
  try {
105
- await fs.rename(temporary, filePath);
27
+ if (exclusive) {
28
+ await fs.link(temporary, filePath);
29
+ await fs.remove(temporary);
30
+ }
31
+ else {
32
+ await fs.rename(temporary, filePath);
33
+ }
106
34
  }
107
35
  catch (error) {
108
36
  await fs.remove(temporary);
@@ -115,164 +43,255 @@ async function clearWorktree(repositoryRoot) {
115
43
  await fs.remove(path.join(repositoryRoot, name));
116
44
  }
117
45
  }
118
- async function materializeSourceHead(options) {
119
- const archivePath = path.join(options.temporaryRoot, 'source-head.tar');
120
- await fs.ensureDir(options.outputRoot);
121
- await git(options.repoRoot, [
122
- 'archive', '--format=tar', `--output=${archivePath}`, options.sourceHead,
123
- ], options.env, 'Could not archive the committed source HEAD.');
124
- const tarEnv = { ...options.env };
125
- delete tarEnv.TAR_OPTIONS;
126
- try {
127
- await execFileAsync('tar', ['-xf', archivePath, '-C', options.outputRoot], {
128
- env: tarEnv,
129
- encoding: 'utf8',
130
- maxBuffer: 16 * 1024 * 1024,
131
- });
46
+ async function assertNoGitAttributes(root, relative = '') {
47
+ for (const entry of await fs.readdir(path.join(root, relative), { withFileTypes: true })) {
48
+ const child = relative ? path.posix.join(relative, entry.name) : entry.name;
49
+ if (entry.name === '.gitattributes') {
50
+ throw new Error(`Catalog publication snapshot must not contain .gitattributes (${child}).`);
51
+ }
52
+ if (entry.isDirectory())
53
+ await assertNoGitAttributes(root, child);
132
54
  }
133
- catch (error) {
134
- throw new Error('Could not materialize the committed source HEAD.', { cause: error });
55
+ }
56
+ async function assertSnapshotBinding(snapshotRoot, descriptor) {
57
+ const [stats, validation, ledgerBytes] = await Promise.all([
58
+ fs.lstat(snapshotRoot),
59
+ validatePublishedCatalogSnapshot(snapshotRoot),
60
+ fs.readFile(path.join(snapshotRoot, 'plugin-release-ledger.json')),
61
+ ]);
62
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
63
+ throw new Error('Prepared Catalog snapshot must be a regular local directory.');
135
64
  }
136
- finally {
137
- await fs.remove(archivePath);
65
+ const release = validation.manifest.release;
66
+ if (validation.manifest.catalogVersion !== descriptor.catalogVersion
67
+ || validation.manifest.channel !== descriptor.channel
68
+ || validation.snapshotDigest !== descriptor.snapshotDigest
69
+ || release.state !== 'published'
70
+ || release.sourceHead !== descriptor.sourceHead
71
+ || release.contentDigest !== descriptor.contentDigest
72
+ || release.planId !== descriptor.planId
73
+ || digest(ledgerBytes) !== descriptor.ledgerDigest) {
74
+ throw new Error('Prepared Catalog descriptor, snapshot, ledger, or plan identity changed.');
138
75
  }
76
+ await assertNoGitAttributes(snapshotRoot);
139
77
  }
140
- async function validatePublishedSourceWorkspace(workspace, env) {
141
- const validator = path.join(resolveCliPackageRoot(), 'dist', 'authoring', 'scripts', 'validate-plugin.mjs');
142
- for (const plugin of workspace.plugins) {
143
- try {
144
- await execFileAsync(process.execPath, [
145
- validator,
146
- plugin.rootDir,
147
- '--workspace-root', workspace.workspaceRoot,
148
- '--json',
149
- ], {
150
- cwd: workspace.workspaceRoot,
151
- env,
152
- encoding: 'utf8',
153
- maxBuffer: 16 * 1024 * 1024,
154
- });
155
- }
156
- catch (error) {
157
- let details = '';
158
- if (typeof error === 'object' && error !== null && 'stdout' in error && typeof error.stdout === 'string') {
159
- try {
160
- const result = JSON.parse(error.stdout);
161
- details = (result.errors ?? [])
162
- .map((entry) => typeof entry.message === 'string' ? entry.message : '')
163
- .filter(Boolean)
164
- .join('; ');
165
- }
166
- catch {
167
- // Keep the public validator failure bounded to its plugin identity.
168
- }
169
- }
170
- throw new Error(`Plugin ${plugin.manifest.name} failed public source validation before Catalog publication${details ? `: ${details}` : '.'}`);
78
+ async function publishCatalogPublicationPreparation(options) {
79
+ const { preparation: inspection, descriptor, snapshotRoot } = options;
80
+ const { temporaryRoot, repositoryRoot, repoRoot, sourceHead, remote, branch, branchRef, tag, tagRef, fetchUrl, pushUrl, safeFetchUrl, safePushUrl, remoteIdentityDigest, parent, attemptPath, env, } = inspection;
81
+ if (catalogPublicationDescriptorDigest(inspection.descriptor)
82
+ !== catalogPublicationDescriptorDigest(descriptor)) {
83
+ throw new Error('Catalog publication descriptor changed after preparation.');
84
+ }
85
+ if (!inspection.tagAvailable) {
86
+ throw new Error(`Remote Catalog tag ${tag} already exists. Increase the Catalog version.`);
87
+ }
88
+ if (!inspection.attemptAvailable || await fs.pathExists(attemptPath)) {
89
+ throw new Error(`Catalog ${descriptor.catalogVersion} already has an attempted publication record. `
90
+ + 'Run aw catalog version <higher> before publishing again.');
91
+ }
92
+ if (!(await publicationGitExit(repoRoot, ['check-ignore', '-q', attemptPath], env))) {
93
+ throw new Error('Catalog publish state under .aw/publish must be ignored by Git. Add .aw/ to .gitignore.');
94
+ }
95
+ await assertSnapshotBinding(snapshotRoot, descriptor);
96
+ if (parent) {
97
+ await publicationGit(repositoryRoot, [
98
+ 'checkout', '--quiet', '--detach', 'refs/aw/catalog-parent',
99
+ ], env, 'publicationCommitFailed', 'Could not select the publication parent.');
100
+ }
101
+ else {
102
+ await publicationGit(repositoryRoot, [
103
+ 'checkout', '--quiet', '--orphan', 'catalog-publication',
104
+ ], env, 'publicationCommitFailed', 'Could not create the initial Catalog publication.');
105
+ }
106
+ await clearWorktree(repositoryRoot);
107
+ await fs.copy(snapshotRoot, repositoryRoot, { dereference: false });
108
+ await publicationGit(repositoryRoot, ['add', '--force', '--all'], env, 'publicationCommitFailed', 'Could not stage the Catalog publication tree.');
109
+ await publicationGit(repositoryRoot, [
110
+ '-c', 'user.name=AW Catalog Publisher',
111
+ '-c', 'user.email=catalog-publisher@agent-workflows.invalid',
112
+ 'commit', '--quiet', '-m', `Catalog ${descriptor.catalogVersion}`,
113
+ ], env, 'publicationCommitFailed', 'Could not create the Catalog publication commit.');
114
+ const commit = await publicationGit(repositoryRoot, [
115
+ 'rev-parse', 'HEAD',
116
+ ], env, 'publicationCommitFailed', 'Could not resolve the Catalog publication commit.');
117
+ const parents = (await publicationGit(repositoryRoot, [
118
+ 'show', '-s', '--format=%P', commit,
119
+ ], env, 'publicationCommitFailed', 'Could not verify the Catalog publication parent.'))
120
+ .split(/\s+/u)
121
+ .filter(Boolean);
122
+ if ((parent && (parents.length !== 1 || parents[0] !== parent)) || (!parent && parents.length !== 0)) {
123
+ throw new Error('Catalog publication commit has an invalid parent.');
124
+ }
125
+ const verificationRoot = path.join(temporaryRoot, 'verification');
126
+ await fs.ensureDir(verificationRoot);
127
+ await publicationGit(repositoryRoot, [
128
+ '--work-tree', verificationRoot, 'checkout', '--quiet', commit, '--', '.',
129
+ ], env, 'publicationCommitFailed', 'Could not verify the Catalog publication commit tree.');
130
+ await assertSnapshotBinding(verificationRoot, descriptor);
131
+ await publicationGit(repositoryRoot, [
132
+ '-c', 'user.name=AW Catalog Publisher',
133
+ '-c', 'user.email=catalog-publisher@agent-workflows.invalid',
134
+ 'tag', '--annotate', tag, '--message', `Catalog ${descriptor.catalogVersion}`,
135
+ ], env, 'publicationCommitFailed', 'Could not create the annotated Catalog tag.');
136
+ const liveHead = await publicationGit(repoRoot, [
137
+ 'rev-parse', '--verify', 'HEAD',
138
+ ], env, 'sourceDrift', 'Could not recheck the source Git HEAD.');
139
+ const liveStatus = await publicationGit(repoRoot, [
140
+ 'status', '--porcelain=v1', '--untracked-files=all', '--ignore-submodules=none',
141
+ ], env, 'sourceDrift', 'Could not recheck the source worktree.');
142
+ if (liveHead !== sourceHead || liveStatus !== '') {
143
+ throw new Error('Source HEAD or worktree changed before Catalog publication.');
144
+ }
145
+ let liveFetchUrl;
146
+ let livePushUrl;
147
+ if (descriptor.remote.name === null) {
148
+ liveFetchUrl = fetchUrl;
149
+ livePushUrl = pushUrl;
150
+ }
151
+ else {
152
+ [liveFetchUrl, livePushUrl] = await Promise.all([
153
+ publicationGit(repoRoot, ['remote', 'get-url', descriptor.remote.name], env, 'remoteDrift', 'Could not recheck the Catalog publication remote configuration.'),
154
+ publicationGit(repoRoot, ['remote', 'get-url', '--push', descriptor.remote.name], env, 'remoteDrift', 'Could not recheck the Catalog publication remote configuration.'),
155
+ ]);
156
+ }
157
+ if (digest(JSON.stringify({ fetchUrl: liveFetchUrl, pushUrl: livePushUrl })) !== remoteIdentityDigest) {
158
+ throw new Error('Catalog remote configuration changed during preparation.');
159
+ }
160
+ const liveRefs = parseRemoteRefs(await publicationGit(repositoryRoot, [
161
+ 'ls-remote', 'publish', branchRef, tagRef, `${tagRef}^{}`,
162
+ ], env, 'remoteDrift', 'Could not recheck the Catalog publication remote.'));
163
+ if ((liveRefs.get(branchRef) ?? null) !== parent || liveRefs.has(tagRef) || liveRefs.has(`${tagRef}^{}`)) {
164
+ throw new Error('Catalog remote changed during preparation. Increase the Catalog version before publishing again.');
165
+ }
166
+ if (await fs.pathExists(attemptPath)) {
167
+ throw new Error(`Catalog ${descriptor.catalogVersion} already has an attempted publication record. `
168
+ + 'Run aw catalog version <higher> before publishing again.');
169
+ }
170
+ await options.beforeRemoteWrite?.();
171
+ const attempted = {
172
+ schemaVersion: 1,
173
+ state: 'attempted',
174
+ workspaceIdentity: descriptor.workspaceIdentity,
175
+ catalogVersion: descriptor.catalogVersion,
176
+ sourceHead,
177
+ remote: { name: descriptor.remote.name, identityDigest: remoteIdentityDigest },
178
+ branch,
179
+ parent,
180
+ tag,
181
+ snapshotDigest: descriptor.snapshotDigest,
182
+ contentDigest: descriptor.contentDigest,
183
+ planId: descriptor.planId,
184
+ descriptorDigest: catalogPublicationDescriptorDigest(descriptor),
185
+ attemptedAt: new Date().toISOString(),
186
+ };
187
+ try {
188
+ await writeJsonAtomic(attemptPath, attempted, true);
189
+ }
190
+ catch (error) {
191
+ if (error.code === 'EEXIST') {
192
+ throw new Error(`Catalog ${descriptor.catalogVersion} already has an attempted publication record. `
193
+ + 'Run aw catalog version <higher> before publishing again.');
171
194
  }
195
+ throw error;
196
+ }
197
+ try {
198
+ await publicationGit(repositoryRoot, [
199
+ 'push', '--atomic', `--force-with-lease=${branchRef}:${parent ?? ''}`,
200
+ 'publish', `${commit}:${branchRef}`, `${tagRef}:${tagRef}`,
201
+ ], env, 'publicationUncertain', `Catalog atomic push failed or its result is uncertain. Do not retry ${descriptor.catalogVersion}; increase the Catalog version.`);
172
202
  }
203
+ catch {
204
+ throw new Error(`Catalog atomic push failed or its result is uncertain. Do not retry ${descriptor.catalogVersion}; run aw catalog version <higher>.`);
205
+ }
206
+ const publishedRefs = parseRemoteRefs(await publicationGit(repositoryRoot, [
207
+ 'ls-remote', 'publish', branchRef, tagRef, `${tagRef}^{}`,
208
+ ], env, 'publicationUncertain', `Catalog publication result is uncertain. Do not retry ${descriptor.catalogVersion}; increase the Catalog version.`));
209
+ if (publishedRefs.get(branchRef) !== commit || publishedRefs.get(`${tagRef}^{}`) !== commit) {
210
+ throw new Error(`Catalog publication result is uncertain. Do not retry ${descriptor.catalogVersion}; run aw catalog version <higher>.`);
211
+ }
212
+ await writeJsonAtomic(attemptPath, {
213
+ ...attempted,
214
+ state: 'published',
215
+ commit,
216
+ publishedAt: new Date().toISOString(),
217
+ }, false);
218
+ return {
219
+ command: 'catalog:publish',
220
+ ok: true,
221
+ workspaceIdentity: descriptor.workspaceIdentity,
222
+ catalogVersion: descriptor.catalogVersion,
223
+ channel: descriptor.channel,
224
+ remote,
225
+ remoteUrl: safeFetchUrl,
226
+ fetchUrl: safeFetchUrl,
227
+ pushUrl: safePushUrl,
228
+ branch,
229
+ parent,
230
+ tag,
231
+ commit,
232
+ sourceHead,
233
+ pluginCount: descriptor.pluginNames.length,
234
+ snapshotDigest: descriptor.snapshotDigest,
235
+ planId: descriptor.planId,
236
+ descriptorDigest: catalogPublicationDescriptorDigest(descriptor),
237
+ };
173
238
  }
174
- const publishBuildAdapter = {
175
- async build(options) {
176
- await bundleLocalPlugin(options.plugin, options.outputRoot, {
177
- buildMode: 'staging',
178
- runtimeResolver: createCatalogRuntimeImportResolver(options.plugins),
239
+ export async function publishPreparedCatalogPublication(options) {
240
+ const inspection = await prepareCatalogPublicationInspection({
241
+ workspaceRoot: options.workspaceRoot,
242
+ remote: options.remote,
243
+ remoteUrl: options.remoteUrl,
244
+ branch: options.branch,
245
+ env: options.env,
246
+ failOnUnavailable: true,
247
+ });
248
+ try {
249
+ return await publishCatalogPublicationPreparation({
250
+ preparation: inspection,
251
+ descriptor: options.descriptor,
252
+ snapshotRoot: options.snapshotRoot,
253
+ beforeRemoteWrite: options.beforeRemoteWrite,
179
254
  });
180
- },
181
- };
255
+ }
256
+ finally {
257
+ await disposeCatalogPublicationInspection(inspection);
258
+ }
259
+ }
182
260
  export async function runCatalogPublishCommand(options) {
183
261
  if (options.json && !options.yes)
184
262
  throw new Error('Catalog publish with --json requires --yes.');
185
263
  if (!options.interactive && !options.yes)
186
264
  throw new Error('Non-interactive Catalog publish requires --yes.');
187
- const remote = options.remote ?? 'origin';
188
- const branch = options.branch ?? 'catalog';
189
- const env = sanitizedGitEnvironment(options.env);
190
- const workspaceRoot = await fs.realpath(path.resolve(options.workspaceRoot));
191
- const repoRoot = await git(workspaceRoot, ['rev-parse', '--show-toplevel'], env, 'Workspace must be inside a real Git worktree.');
192
- const sourceHead = await git(repoRoot, ['rev-parse', '--verify', 'HEAD'], env, 'Workspace Git HEAD must be committed and not unborn.');
193
- const status = await git(repoRoot, [
194
- 'status', '--porcelain=v1', '--untracked-files=all', '--ignore-submodules=none',
195
- ], env, 'Could not inspect the source worktree.');
196
- if (status !== '')
197
- throw new Error('Catalog publish requires the complete source Git worktree to be clean.');
198
- const temporaryRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aw-public-catalog-'));
199
- let fetchUrl = '';
200
- let pushUrl = '';
201
- let safeFetchUrl = '';
202
- let safePushUrl = '';
265
+ if (!/^sha256:[0-9a-f]{64}$/u.test(options.expectedDescriptorDigest ?? '')) {
266
+ throw new Error('Catalog publish requires --expected-descriptor-digest from a successful publication preflight.');
267
+ }
268
+ const inspection = await prepareCatalogPublicationInspection({
269
+ ...options,
270
+ failOnUnavailable: true,
271
+ });
203
272
  try {
204
- const sourceRoot = path.join(temporaryRoot, 'source');
205
- await materializeSourceHead({ repoRoot, sourceHead, outputRoot: sourceRoot, temporaryRoot, env });
206
- const workspaceRelative = path.relative(repoRoot, workspaceRoot);
207
- if (workspaceRelative === '..' || workspaceRelative.startsWith(`..${path.sep}`) || path.isAbsolute(workspaceRelative)) {
208
- throw new Error('Catalog workspace must stay inside its Git worktree root.');
273
+ const preparedDescriptorDigest = catalogPublicationDescriptorDigest(inspection.descriptor);
274
+ if (options.expectedDescriptorDigest !== preparedDescriptorDigest) {
275
+ throw new Error('Catalog publication descriptor does not match the authorized preflight. Run preflight and authorize the current exact identity.');
209
276
  }
210
- const sourceWorkspaceRoot = path.join(sourceRoot, workspaceRelative);
211
- await git(repoRoot, ['check-ref-format', '--branch', branch], env, 'Invalid Catalog publication branch name.');
212
- const workspace = await readPluginWorkspace(sourceWorkspaceRoot, { reservedImports: CLI_RESERVED_IMPORTS });
213
- await validatePublishedSourceWorkspace(workspace, env);
214
- const catalogVersion = workspace.metadata.catalog.version;
215
- const tag = `catalog-v${catalogVersion}`;
216
- await git(repoRoot, ['check-ref-format', `refs/tags/${tag}`], env, 'Invalid Catalog publication tag.');
217
- const markerPath = attemptPath(workspaceRoot, catalogVersion);
218
- if (!(await gitExit(repoRoot, ['check-ignore', '-q', markerPath], env))) {
277
+ if (!(await publicationGitExit(inspection.repoRoot, ['check-ignore', '-q', inspection.attemptPath], inspection.env))) {
219
278
  throw new Error('Catalog publish state under .aw/publish must be ignored by Git. Add .aw/ to .gitignore.');
220
279
  }
221
- await migrateLegacyAttempt(workspaceRoot, catalogVersion);
222
- if (await fs.pathExists(markerPath)) {
223
- throw new Error(`Catalog ${catalogVersion} already has an attempted publication record. `
224
- + 'Run aw catalog version <higher> before publishing again.');
225
- }
226
- fetchUrl = await git(repoRoot, ['remote', 'get-url', remote], env, `Git remote ${JSON.stringify(remote)} is not configured.`);
227
- pushUrl = await git(repoRoot, ['remote', 'get-url', '--push', remote], env, `Git remote ${JSON.stringify(remote)} has no push URL.`);
228
- safeFetchUrl = safeRemoteIdentity(fetchUrl);
229
- safePushUrl = safeRemoteIdentity(pushUrl);
230
- const remoteIdentityDigest = digest(JSON.stringify({ fetchUrl, pushUrl }));
231
- const branchRef = `refs/heads/${branch}`;
232
- const tagRef = `refs/tags/${tag}`;
233
- const repositoryRoot = path.join(temporaryRoot, 'repository');
234
- await fs.ensureDir(repositoryRoot);
235
- await git(repositoryRoot, ['init', '--quiet'], env, 'Could not create the isolated publication repository.');
236
- await git(repositoryRoot, ['config', 'core.hooksPath', path.join(temporaryRoot, 'disabled-hooks')], env, 'Could not disable publication hooks.');
237
- await git(repositoryRoot, ['config', 'core.symlinks', 'true'], env, 'Could not configure the isolated publication repository.');
238
- await git(repositoryRoot, ['remote', 'add', 'publish', pushUrl], env, 'Could not configure the publication remote.');
239
- const initialRefs = parseRemoteRefs(await git(repositoryRoot, [
240
- 'ls-remote', 'publish', branchRef, tagRef, `${tagRef}^{}`,
241
- ], env, 'Could not inspect the Catalog publication remote.'));
242
- if (initialRefs.has(tagRef) || initialRefs.has(`${tagRef}^{}`)) {
243
- throw new Error(`Remote Catalog tag ${tag} already exists. Increase the Catalog version.`);
244
- }
245
- const parent = initialRefs.get(branchRef) ?? null;
246
- let previousSnapshotRoot;
247
- if (parent) {
248
- await git(repositoryRoot, ['fetch', '--quiet', '--no-tags', 'publish', `${branchRef}:refs/remotes/publish/catalog`], env, 'Could not fetch the published Catalog branch.');
249
- previousSnapshotRoot = path.join(temporaryRoot, 'previous');
250
- await fs.ensureDir(previousSnapshotRoot);
251
- await git(repositoryRoot, ['--work-tree', previousSnapshotRoot, 'checkout', '--quiet', 'refs/remotes/publish/catalog', '--', '.'], env, 'Could not read the published Catalog snapshot.');
252
- await validatePublishedCatalogSnapshot(previousSnapshotRoot);
253
- }
254
- const prepared = await preparePublishedWorkspaceCatalog({
255
- workspaceRoot: sourceWorkspaceRoot,
256
- sourceHead,
257
- previousSnapshotRoot,
258
- adapter: publishBuildAdapter,
259
- reservedImports: CLI_RESERVED_IMPORTS,
260
- });
261
- const preparedHead = await git(repoRoot, ['rev-parse', '--verify', 'HEAD'], env, 'Could not recheck the source Git HEAD.');
262
- const preparedStatus = await git(repoRoot, [
263
- 'status', '--porcelain=v1', '--untracked-files=all', '--ignore-submodules=none',
264
- ], env, 'Could not recheck the source worktree.');
265
- if (preparedHead !== sourceHead || preparedStatus !== '') {
266
- throw new Error('Source HEAD or worktree changed during Catalog preparation. Commit or clean it before publishing.');
267
- }
268
280
  if (!options.yes) {
269
281
  options.io.stderr([
270
- `Source HEAD: ${sourceHead}`,
271
- `Catalog: ${prepared.catalogVersion} (${prepared.channel})`,
272
- `Push remote: ${remote} (${safePushUrl})`,
273
- `Branch: ${branchRef}`,
274
- `Tag: ${tagRef}`,
275
- `Plugins: ${prepared.pluginNames.length}`,
282
+ `Workspace identity: ${inspection.descriptor.workspaceIdentity}`,
283
+ `Source HEAD: ${inspection.sourceHead}`,
284
+ `Catalog: ${inspection.descriptor.catalogVersion} (${inspection.descriptor.channel})`,
285
+ `Remote: ${inspection.remote}`,
286
+ `Fetch URL: ${inspection.safeFetchUrl}`,
287
+ `Push URL: ${inspection.safePushUrl}`,
288
+ `Branch: ${inspection.branchRef}`,
289
+ `Parent: ${inspection.parent ?? '<root>'}`,
290
+ `Tag: ${inspection.tagRef}`,
291
+ `Snapshot digest: ${inspection.descriptor.snapshotDigest}`,
292
+ `Plan ID: ${inspection.descriptor.planId}`,
293
+ `Descriptor digest: ${preparedDescriptorDigest}`,
294
+ `Plugins: ${inspection.descriptor.pluginNames.length}`,
276
295
  'One atomic push will publish the branch and annotated tag.',
277
296
  ].join('\n') + '\n');
278
297
  if (!options.confirm || !(await options.confirm('Publish this Catalog snapshot?'))) {
@@ -280,95 +299,21 @@ export async function runCatalogPublishCommand(options) {
280
299
  return null;
281
300
  }
282
301
  }
283
- if (parent)
284
- await git(repositoryRoot, ['checkout', '--quiet', '--detach', 'refs/remotes/publish/catalog'], env, 'Could not select the publication parent.');
285
- else
286
- await git(repositoryRoot, ['checkout', '--quiet', '--orphan', 'catalog-publication'], env, 'Could not create the initial Catalog publication.');
287
- await clearWorktree(repositoryRoot);
288
- await fs.copy(prepared.outputRoot, repositoryRoot, { dereference: false });
289
- await git(repositoryRoot, ['add', '--force', '--all'], env, 'Could not stage the Catalog publication tree.');
290
- await git(repositoryRoot, [
291
- '-c', 'user.name=AW Catalog Publisher',
292
- '-c', 'user.email=catalog-publisher@agent-workflows.invalid',
293
- 'commit', '--quiet', '-m', `Catalog ${catalogVersion}`,
294
- ], env, 'Could not create the Catalog publication commit.');
295
- const commit = await git(repositoryRoot, ['rev-parse', 'HEAD'], env, 'Could not resolve the Catalog publication commit.');
296
- const parents = (await git(repositoryRoot, ['show', '-s', '--format=%P', commit], env, 'Could not verify the Catalog publication parent.')).split(/\s+/u).filter(Boolean);
297
- if ((parent && (parents.length !== 1 || parents[0] !== parent)) || (!parent && parents.length !== 0)) {
298
- throw new Error('Catalog publication commit has an invalid parent.');
299
- }
300
- const verificationRoot = path.join(temporaryRoot, 'verification');
301
- await fs.ensureDir(verificationRoot);
302
- await git(repositoryRoot, ['--work-tree', verificationRoot, 'checkout', '--quiet', commit, '--', '.'], env, 'Could not verify the Catalog publication commit tree.');
303
- const committed = await validatePublishedCatalogSnapshot(verificationRoot);
304
- if (committed.snapshotDigest !== prepared.snapshotDigest)
305
- throw new Error('Catalog publication commit tree differs from the prepared snapshot.');
306
- await git(repositoryRoot, [
307
- '-c', 'user.name=AW Catalog Publisher',
308
- '-c', 'user.email=catalog-publisher@agent-workflows.invalid',
309
- 'tag', '--annotate', tag, '--message', `Catalog ${catalogVersion}`,
310
- ], env, 'Could not create the annotated Catalog tag.');
311
- const attemptedAt = new Date().toISOString();
312
- const attempted = {
313
- schemaVersion: 1,
314
- state: 'attempted',
315
- workspaceIdentity: digest(JSON.stringify({ repoRoot, workspaceRelative: path.relative(repoRoot, workspaceRoot) })),
316
- catalogVersion,
317
- sourceHead,
318
- remote: { name: remote, identityDigest: remoteIdentityDigest },
319
- branch,
320
- tag,
321
- preparedSnapshotDigest: prepared.snapshotDigest,
322
- attemptedAt,
323
- };
324
- await writeJsonAtomic(markerPath, attempted);
325
- const liveRefs = parseRemoteRefs(await git(repositoryRoot, [
326
- 'ls-remote', 'publish', branchRef, tagRef, `${tagRef}^{}`,
327
- ], env, 'Could not recheck the Catalog publication remote.'));
328
- if ((liveRefs.get(branchRef) ?? null) !== parent || liveRefs.has(tagRef) || liveRefs.has(`${tagRef}^{}`)) {
329
- throw new Error('Catalog remote changed during preparation. Increase the Catalog version before publishing again.');
330
- }
331
- try {
332
- await git(repositoryRoot, [
333
- 'push', '--atomic', 'publish', `${commit}:${branchRef}`, `${tagRef}:${tagRef}`,
334
- ], env, `Catalog atomic push failed or its result is uncertain. Do not retry ${catalogVersion}; increase the Catalog version.`);
335
- }
336
- catch {
337
- throw new Error(`Catalog atomic push failed or its result is uncertain. Do not retry ${catalogVersion}; run aw catalog version <higher>.`);
338
- }
339
- const publishedRefs = parseRemoteRefs(await git(repositoryRoot, [
340
- 'ls-remote', 'publish', branchRef, tagRef, `${tagRef}^{}`,
341
- ], env, `Catalog publication result is uncertain. Do not retry ${catalogVersion}; increase the Catalog version.`));
342
- if (publishedRefs.get(branchRef) !== commit || publishedRefs.get(`${tagRef}^{}`) !== commit) {
343
- throw new Error(`Catalog publication result is uncertain. Do not retry ${catalogVersion}; run aw catalog version <higher>.`);
344
- }
345
- await writeJsonAtomic(markerPath, { ...attempted, state: 'published', commit, publishedAt: new Date().toISOString() });
346
- return {
347
- command: 'catalog:publish',
348
- ok: true,
349
- catalogVersion,
350
- channel: prepared.channel,
351
- remote,
352
- remoteUrl: safeFetchUrl,
353
- fetchUrl: safeFetchUrl,
354
- pushUrl: safePushUrl,
355
- branch,
356
- tag,
357
- commit,
358
- sourceHead,
359
- pluginCount: prepared.pluginNames.length,
360
- snapshotDigest: prepared.snapshotDigest,
361
- };
302
+ return await publishCatalogPublicationPreparation({
303
+ preparation: inspection,
304
+ descriptor: inspection.descriptor,
305
+ snapshotRoot: inspection.prepared.outputRoot,
306
+ });
362
307
  }
363
308
  catch (error) {
364
- const message = error instanceof Error ? error.message : String(error);
365
- throw new Error(message
366
- .replaceAll(temporaryRoot, '<temporary Catalog state>')
367
- .replaceAll(fetchUrl, safeFetchUrl)
368
- .replaceAll(pushUrl, safePushUrl));
309
+ const message = (error instanceof Error ? error.message : String(error))
310
+ .replaceAll(inspection.temporaryRoot, '<temporary Catalog state>')
311
+ .replaceAll(inspection.fetchUrl, inspection.safeFetchUrl)
312
+ .replaceAll(inspection.pushUrl, inspection.safePushUrl);
313
+ throw new Error(message);
369
314
  }
370
315
  finally {
371
- await fs.remove(temporaryRoot);
316
+ await disposeCatalogPublicationInspection(inspection);
372
317
  }
373
318
  }
374
319
  //# sourceMappingURL=catalog-publish.js.map