@amodalai/amodal 0.2.1 → 0.2.2

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 (92) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/src/commands/connect-channel.d.ts +11 -0
  3. package/dist/src/commands/connect-channel.d.ts.map +1 -0
  4. package/dist/src/commands/connect-channel.js +169 -0
  5. package/dist/src/commands/connect-channel.js.map +1 -0
  6. package/dist/src/commands/connect.d.ts +3 -1
  7. package/dist/src/commands/connect.d.ts.map +1 -1
  8. package/dist/src/commands/connect.js +27 -16
  9. package/dist/src/commands/connect.js.map +1 -1
  10. package/dist/src/commands/groups/connect.d.ts +8 -0
  11. package/dist/src/commands/groups/connect.d.ts.map +1 -0
  12. package/dist/src/commands/groups/connect.js +17 -0
  13. package/dist/src/commands/groups/connect.js.map +1 -0
  14. package/dist/src/commands/groups/pkg.d.ts.map +1 -1
  15. package/dist/src/commands/groups/pkg.js +0 -12
  16. package/dist/src/commands/groups/pkg.js.map +1 -1
  17. package/dist/src/commands/index.d.ts +1 -1
  18. package/dist/src/commands/index.d.ts.map +1 -1
  19. package/dist/src/commands/index.js +3 -1
  20. package/dist/src/commands/index.js.map +1 -1
  21. package/dist/src/commands/init.d.ts.map +1 -1
  22. package/dist/src/commands/init.js +4 -1
  23. package/dist/src/commands/init.js.map +1 -1
  24. package/dist/src/commands/inspect.d.ts.map +1 -1
  25. package/dist/src/commands/inspect.js +32 -38
  26. package/dist/src/commands/inspect.js.map +1 -1
  27. package/dist/src/commands/install-pkg.d.ts +5 -5
  28. package/dist/src/commands/install-pkg.d.ts.map +1 -1
  29. package/dist/src/commands/install-pkg.js +35 -77
  30. package/dist/src/commands/install-pkg.js.map +1 -1
  31. package/dist/src/commands/uninstall.d.ts +2 -2
  32. package/dist/src/commands/uninstall.d.ts.map +1 -1
  33. package/dist/src/commands/uninstall.js +6 -32
  34. package/dist/src/commands/uninstall.js.map +1 -1
  35. package/dist/src/commands/validate.d.ts.map +1 -1
  36. package/dist/src/commands/validate.js +15 -21
  37. package/dist/src/commands/validate.js.map +1 -1
  38. package/dist/src/shared/tarball.js +1 -1
  39. package/dist/src/shared/tarball.js.map +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +5 -5
  42. package/src/commands/command-exports.test.ts +1 -1
  43. package/src/commands/connect-channel.ts +197 -0
  44. package/src/commands/connect.test.ts +28 -40
  45. package/src/commands/connect.ts +30 -24
  46. package/src/commands/groups/connect.ts +20 -0
  47. package/src/commands/groups/pkg.ts +0 -12
  48. package/src/commands/index.ts +5 -1
  49. package/src/commands/init.test.ts +2 -2
  50. package/src/commands/init.ts +5 -1
  51. package/src/commands/inspect.test.ts +1 -18
  52. package/src/commands/inspect.ts +31 -36
  53. package/src/commands/install-pkg.test.ts +39 -73
  54. package/src/commands/install-pkg.ts +38 -87
  55. package/src/commands/uninstall.test.ts +11 -51
  56. package/src/commands/uninstall.ts +7 -36
  57. package/src/commands/validate.test.ts +5 -26
  58. package/src/commands/validate.ts +15 -20
  59. package/src/e2e-commands.test.ts +0 -1
  60. package/src/e2e-plugins.test.ts +0 -1
  61. package/src/e2e.test.ts +0 -89
  62. package/src/shared/tarball.ts +1 -1
  63. package/dist/src/commands/diff.d.ts +0 -17
  64. package/dist/src/commands/diff.d.ts.map +0 -1
  65. package/dist/src/commands/diff.js +0 -118
  66. package/dist/src/commands/diff.js.map +0 -1
  67. package/dist/src/commands/list.d.ts +0 -18
  68. package/dist/src/commands/list.d.ts.map +0 -1
  69. package/dist/src/commands/list.js +0 -82
  70. package/dist/src/commands/list.js.map +0 -1
  71. package/dist/src/commands/publish.d.ts +0 -18
  72. package/dist/src/commands/publish.d.ts.map +0 -1
  73. package/dist/src/commands/publish.js +0 -121
  74. package/dist/src/commands/publish.js.map +0 -1
  75. package/dist/src/commands/search.d.ts +0 -19
  76. package/dist/src/commands/search.d.ts.map +0 -1
  77. package/dist/src/commands/search.js +0 -97
  78. package/dist/src/commands/search.js.map +0 -1
  79. package/dist/src/commands/update.d.ts +0 -19
  80. package/dist/src/commands/update.d.ts.map +0 -1
  81. package/dist/src/commands/update.js +0 -158
  82. package/dist/src/commands/update.js.map +0 -1
  83. package/src/commands/diff.test.ts +0 -165
  84. package/src/commands/diff.ts +0 -141
  85. package/src/commands/list.test.ts +0 -141
  86. package/src/commands/list.ts +0 -99
  87. package/src/commands/publish.test.ts +0 -169
  88. package/src/commands/publish.ts +0 -141
  89. package/src/commands/search.test.ts +0 -171
  90. package/src/commands/search.ts +0 -120
  91. package/src/commands/update.test.ts +0 -256
  92. package/src/commands/update.ts +0 -196
@@ -1,141 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Amodal Labs, Inc.
4
- * SPDX-License-Identifier: MIT
5
- */
6
-
7
- import {join} from 'node:path';
8
-
9
- import type {CommandModule} from 'yargs';
10
- import {
11
- ensureNpmContext,
12
- fromNpmName,
13
- getLockEntry,
14
- getNpmContextPaths,
15
- npmView,
16
- readPackageFile,
17
- listPackageFiles,
18
- toNpmName,
19
- } from '@amodalai/core';
20
- import {findRepoRoot} from '../shared/repo-discovery.js';
21
-
22
- export interface DiffOptions {
23
- cwd?: string;
24
- name: string;
25
- }
26
-
27
- /**
28
- * Show diff between installed version and latest available.
29
- * Returns 0 on success, 1 on error.
30
- */
31
- export async function runDiff(options: DiffOptions): Promise<number> {
32
- let repoPath: string;
33
- try {
34
- repoPath = findRepoRoot(options.cwd);
35
- } catch (err) {
36
- const msg = err instanceof Error ? err.message : String(err);
37
- process.stderr.write(`[diff] ${msg}\n`);
38
- return 1;
39
- }
40
-
41
- const paths = await ensureNpmContext(repoPath);
42
- const npmName = toNpmName(options.name);
43
- const shortName = fromNpmName(npmName);
44
-
45
- // Check if installed
46
- const lockEntry = await getLockEntry(repoPath, npmName);
47
- if (!lockEntry) {
48
- process.stderr.write(`[diff] Package ${npmName} is not installed.\n`);
49
- return 1;
50
- }
51
-
52
- // Get latest version info
53
- let latestVersion: string;
54
- try {
55
- const viewResult = await npmView(paths, npmName);
56
- latestVersion = viewResult.version;
57
- } catch (err) {
58
- const msg = err instanceof Error ? err.message : String(err);
59
- process.stderr.write(`[diff] Failed to query registry: ${msg}\n`);
60
- return 1;
61
- }
62
-
63
- if (latestVersion === lockEntry.version) {
64
- process.stderr.write(`[diff] ${npmName}@${lockEntry.version} is already the latest version.\n`);
65
- return 0;
66
- }
67
-
68
- process.stderr.write(`[diff] Comparing ${npmName}: ${lockEntry.version} (installed) → ${latestVersion} (latest)\n`);
69
-
70
- // Get installed package directory
71
- const contextPaths = getNpmContextPaths(repoPath);
72
- const packageDir = join(contextPaths.nodeModules, npmName);
73
-
74
- // List installed files
75
- let installedFiles: string[];
76
- try {
77
- installedFiles = await listPackageFiles(packageDir);
78
- } catch {
79
- installedFiles = [];
80
- }
81
-
82
- // Print diff report
83
- process.stdout.write(`\n ${npmName}\n`);
84
- process.stdout.write(` Installed: ${lockEntry.version}\n`);
85
- process.stdout.write(` Latest: ${latestVersion}\n\n`);
86
-
87
- // Show file-level summary
88
- process.stdout.write(' Files in installed version:\n');
89
- for (const file of installedFiles) {
90
- const content = await readPackageFile(packageDir, file);
91
- if (content !== null) {
92
- const lineCount = content.split('\n').length;
93
- let detail = `${lineCount} lines`;
94
-
95
- // Add type-specific details
96
- if (file === 'surface.md') {
97
- const endpointCount = (content.match(/^##\s/gm) ?? []).length;
98
- detail += `, ${endpointCount} endpoint${endpointCount === 1 ? '' : 's'}`;
99
- } else if (file === 'spec.json') {
100
- try {
101
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
102
- const spec = JSON.parse(content) as Record<string, unknown>;
103
- const keys = Object.keys(spec);
104
- detail += `, keys: ${keys.join(', ')}`;
105
- } catch {
106
- // Not valid JSON
107
- }
108
- } else if (file === 'access.json') {
109
- try {
110
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
111
- const access = JSON.parse(content) as Record<string, unknown>;
112
- const ruleCount = Object.keys(access).length;
113
- detail += `, ${ruleCount} rule${ruleCount === 1 ? '' : 's'}`;
114
- } catch {
115
- // Not valid JSON
116
- }
117
- } else if (file === 'entities.md') {
118
- const sectionCount = (content.match(/^##\s/gm) ?? []).length;
119
- detail += `, ${sectionCount} entit${sectionCount === 1 ? 'y' : 'ies'}`;
120
- }
121
-
122
- process.stdout.write(` ${file} (${detail})\n`);
123
- }
124
- }
125
-
126
- process.stdout.write(`\n Run \`amodal update ${shortName}\` to upgrade.\n\n`);
127
- return 0;
128
- }
129
-
130
- export const diffCommand: CommandModule = {
131
- command: 'diff <name>',
132
- describe: 'Show diff between installed and latest version',
133
- builder: (yargs) =>
134
- yargs
135
- .positional('name', {type: 'string', demandOption: true, describe: 'Package name'}),
136
- handler: async (argv) => {
137
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
138
- const code = await runDiff({name: argv['name'] as string});
139
- process.exit(code);
140
- },
141
- };
@@ -1,141 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Amodal Labs, Inc.
4
- * SPDX-License-Identifier: MIT
5
- */
6
-
7
- import {describe, it, expect, vi, beforeEach} from 'vitest';
8
-
9
- const mockFindRepoRoot = vi.fn(() => '/test/repo');
10
- const mockListLockEntries = vi.fn();
11
- const mockFromNpmName = vi.fn((npm: string) => npm.replace('@amodalai/', ''));
12
-
13
- vi.mock('../shared/repo-discovery.js', () => ({
14
- findRepoRoot: mockFindRepoRoot,
15
- }));
16
-
17
- vi.mock('@amodalai/core', () => ({
18
- listLockEntries: mockListLockEntries,
19
- fromNpmName: mockFromNpmName,
20
- }));
21
-
22
- describe('runList', () => {
23
- let stdoutOutput: string;
24
- let stderrOutput: string;
25
-
26
- beforeEach(() => {
27
- vi.clearAllMocks();
28
- mockFindRepoRoot.mockReturnValue('/test/repo');
29
- stdoutOutput = '';
30
- stderrOutput = '';
31
- vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
32
- stdoutOutput += String(chunk);
33
- return true;
34
- });
35
- vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
36
- stderrOutput += String(chunk);
37
- return true;
38
- });
39
- });
40
-
41
- it('should print "No packages installed" when no entries', async () => {
42
- mockListLockEntries.mockResolvedValue([]);
43
-
44
- const {runList} = await import('./list.js');
45
- const result = await runList();
46
- expect(result).toBe(0);
47
- expect(stderrOutput).toContain('No packages installed');
48
- });
49
-
50
- it('should print entries as formatted table', async () => {
51
- mockListLockEntries.mockResolvedValue([
52
- {npmName: '@amodalai/connection-salesforce', entry: {version: '2.1.0', integrity: 'sha512-abc'}},
53
- {npmName: '@amodalai/skill-triage', entry: {version: '1.0.3', integrity: 'sha512-def'}},
54
- ]);
55
-
56
- const {runList} = await import('./list.js');
57
- const result = await runList();
58
- expect(result).toBe(0);
59
- expect(stdoutOutput).toContain('NAME');
60
- expect(stdoutOutput).toContain('connection-salesforce');
61
- expect(stdoutOutput).toContain('skill-triage');
62
- expect(stderrOutput).toContain('2 packages installed');
63
- });
64
-
65
- it('should filter by name substring', async () => {
66
- mockListLockEntries.mockResolvedValue([]);
67
-
68
- const {runList} = await import('./list.js');
69
- const result = await runList({filter: 'connection'});
70
- expect(result).toBe(0);
71
- expect(mockListLockEntries).toHaveBeenCalledWith('/test/repo');
72
- expect(stderrOutput).toContain('No packages matching "connection" installed');
73
- });
74
-
75
- it('should output JSON when json option set', async () => {
76
- mockListLockEntries.mockResolvedValue([
77
- {npmName: '@amodalai/connection-stripe', entry: {version: '1.0.0', integrity: 'sha512-xyz'}},
78
- ]);
79
-
80
- const {runList} = await import('./list.js');
81
- const result = await runList({json: true});
82
- expect(result).toBe(0);
83
-
84
- const parsed = JSON.parse(stdoutOutput);
85
- expect(parsed).toHaveLength(1);
86
- expect(parsed[0]).toMatchObject({name: 'connection-stripe', version: '1.0.0'});
87
- });
88
-
89
- it('should return 1 when repo not found', async () => {
90
- mockFindRepoRoot.mockImplementation(() => {
91
- throw new Error('Not found');
92
- });
93
-
94
- const {runList} = await import('./list.js');
95
- const result = await runList();
96
- expect(result).toBe(1);
97
- expect(stderrOutput).toContain('Not found');
98
- });
99
-
100
- it('should handle single package with correct count', async () => {
101
- mockListLockEntries.mockResolvedValue([
102
- {npmName: '@amodalai/connection-datadog', entry: {version: '3.0.0', integrity: 'sha512-ghi'}},
103
- ]);
104
-
105
- const {runList} = await import('./list.js');
106
- const result = await runList();
107
- expect(result).toBe(0);
108
- expect(stderrOutput).toContain('1 package installed');
109
- expect(stderrOutput).not.toContain('1 packages');
110
- });
111
-
112
- it('should pass cwd to findRepoRoot', async () => {
113
- mockListLockEntries.mockResolvedValue([]);
114
-
115
- const {runList} = await import('./list.js');
116
- await runList({cwd: '/custom/dir'});
117
- expect(mockFindRepoRoot).toHaveBeenCalledWith('/custom/dir');
118
- });
119
-
120
- it('should include integrity in JSON output', async () => {
121
- mockListLockEntries.mockResolvedValue([
122
- {npmName: '@amodalai/skill-analyze', entry: {version: '1.0.0', integrity: 'sha512-integrity-hash'}},
123
- ]);
124
-
125
- const {runList} = await import('./list.js');
126
- await runList({json: true});
127
-
128
- const parsed = JSON.parse(stdoutOutput);
129
- expect(parsed[0]['integrity']).toBe('sha512-integrity-hash');
130
- });
131
-
132
- it('should handle json output with empty list', async () => {
133
- mockListLockEntries.mockResolvedValue([]);
134
-
135
- const {runList} = await import('./list.js');
136
- const result = await runList({json: true});
137
- expect(result).toBe(0);
138
- // No JSON output when empty — prints "no packages" to stderr instead
139
- expect(stderrOutput).toContain('No packages installed');
140
- });
141
- });
@@ -1,99 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Amodal Labs, Inc.
4
- * SPDX-License-Identifier: MIT
5
- */
6
-
7
- import type {CommandModule} from 'yargs';
8
- import {fromNpmName, listLockEntries} from '@amodalai/core';
9
- import {findRepoRoot} from '../shared/repo-discovery.js';
10
-
11
- export interface ListOptions {
12
- cwd?: string;
13
- filter?: string;
14
- json?: boolean;
15
- }
16
-
17
- /**
18
- * List installed packages from the lock file.
19
- * Returns 0 on success, 1 on error.
20
- */
21
- export async function runList(options: ListOptions = {}): Promise<number> {
22
- let repoPath: string;
23
- try {
24
- repoPath = findRepoRoot(options.cwd);
25
- } catch (err) {
26
- const msg = err instanceof Error ? err.message : String(err);
27
- process.stderr.write(`[list] ${msg}\n`);
28
- return 1;
29
- }
30
-
31
- let entries = await listLockEntries(repoPath);
32
-
33
- // Optional string filter on npm name
34
- if (options.filter) {
35
- const f = options.filter.toLowerCase();
36
- entries = entries.filter((e) => e.npmName.toLowerCase().includes(f));
37
- }
38
-
39
- if (entries.length === 0) {
40
- if (options.filter) {
41
- process.stderr.write(`[list] No packages matching "${options.filter}" installed.\n`);
42
- } else {
43
- process.stderr.write('[list] No packages installed.\n');
44
- }
45
- return 0;
46
- }
47
-
48
- if (options.json) {
49
- const output = entries.map((e) => ({
50
- name: fromNpmName(e.npmName),
51
- npmName: e.npmName,
52
- version: e.entry.version,
53
- integrity: e.entry.integrity,
54
- }));
55
- process.stdout.write(JSON.stringify(output, null, 2) + '\n');
56
- return 0;
57
- }
58
-
59
- // Formatted table
60
- const names = entries.map((e) => fromNpmName(e.npmName));
61
- const nameWidth = Math.max(4, ...names.map((n) => n.length));
62
- const npmWidth = Math.max(3, ...entries.map((e) => e.npmName.length));
63
- const versionWidth = Math.max(7, ...entries.map((e) => e.entry.version.length));
64
-
65
- const header = [
66
- 'NAME'.padEnd(nameWidth),
67
- 'VERSION'.padEnd(versionWidth),
68
- 'NPM'.padEnd(npmWidth),
69
- ].join(' ');
70
-
71
- process.stdout.write(header + '\n');
72
-
73
- for (let i = 0; i < entries.length; i++) {
74
- const e = entries[i];
75
- const row = [
76
- names[i].padEnd(nameWidth),
77
- e.entry.version.padEnd(versionWidth),
78
- e.npmName.padEnd(npmWidth),
79
- ].join(' ');
80
- process.stdout.write(row + '\n');
81
- }
82
-
83
- process.stderr.write(`[list] ${entries.length} package${entries.length === 1 ? '' : 's'} installed.\n`);
84
- return 0;
85
- }
86
-
87
- export const listCommand: CommandModule = {
88
- command: 'list',
89
- describe: 'List installed packages',
90
- builder: (yargs) =>
91
- yargs
92
- .option('filter', {type: 'string', describe: 'Filter by name substring'})
93
- .option('json', {type: 'boolean', default: false, describe: 'Output as JSON'}),
94
- handler: async (argv) => {
95
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
96
- const code = await runList({filter: argv['filter'] as string | undefined, json: argv['json'] as boolean});
97
- process.exit(code);
98
- },
99
- };
@@ -1,169 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Amodal Labs, Inc.
4
- * SPDX-License-Identifier: MIT
5
- */
6
-
7
- import {describe, it, expect, vi, beforeEach} from 'vitest';
8
-
9
- const mockStat = vi.fn();
10
- const mockReadFile = vi.fn();
11
- const mockReaddir = vi.fn();
12
- const mockExecFile = vi.fn();
13
-
14
- vi.mock('node:fs/promises', () => ({
15
- stat: mockStat,
16
- readFile: mockReadFile,
17
- readdir: mockReaddir,
18
- }));
19
-
20
- vi.mock('node:child_process', () => ({
21
- execFile: mockExecFile,
22
- }));
23
-
24
- const validPkgJson = JSON.stringify({
25
- name: '@amodalai/connection-salesforce',
26
- version: '1.0.0',
27
- amodal: {name: 'salesforce', tags: ['connection']},
28
- });
29
-
30
- describe('runPublish', () => {
31
- let stderrOutput: string;
32
-
33
- beforeEach(() => {
34
- vi.clearAllMocks();
35
- mockStat.mockResolvedValue({isFile: () => true, isDirectory: () => true});
36
- mockReadFile.mockResolvedValue(validPkgJson);
37
- mockReaddir.mockResolvedValue([]);
38
- stderrOutput = '';
39
- vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
40
- stderrOutput += String(chunk);
41
- return true;
42
- });
43
- });
44
-
45
- it('should publish successfully', async () => {
46
- mockExecFile.mockImplementation(
47
- (_cmd: string, _args: string[], _opts: unknown, cb: (err: Error | null, result: {stdout: string; stderr: string}) => void) => {
48
- cb(null, {stdout: '', stderr: ''});
49
- return {};
50
- },
51
- );
52
-
53
- const {runPublish} = await import('./publish.js');
54
- const result = await runPublish();
55
- expect(result).toBe(0);
56
- expect(stderrOutput).toContain('Published');
57
- });
58
-
59
- it('should show dry run output', async () => {
60
- const {runPublish} = await import('./publish.js');
61
- const result = await runPublish({dryRun: true});
62
- expect(result).toBe(0);
63
- expect(stderrOutput).toContain('Dry run');
64
- expect(stderrOutput).toContain('salesforce');
65
- expect(mockExecFile).not.toHaveBeenCalled();
66
- });
67
-
68
- it('should return 1 when package.json is unreadable', async () => {
69
- mockReadFile.mockRejectedValue(new Error('ENOENT'));
70
-
71
- const {runPublish} = await import('./publish.js');
72
- const result = await runPublish();
73
- expect(result).toBe(1);
74
- expect(stderrOutput).toContain('Failed to read package.json');
75
- });
76
-
77
- it('should return 1 when amodal block missing', async () => {
78
- mockReadFile.mockResolvedValue(JSON.stringify({
79
- name: '@amodalai/test',
80
- version: '1.0.0',
81
- }));
82
-
83
- const {runPublish} = await import('./publish.js');
84
- const result = await runPublish();
85
- expect(result).toBe(1);
86
- expect(stderrOutput).toContain('amodal');
87
- });
88
-
89
- it('should return 1 when amodal name missing', async () => {
90
- mockReadFile.mockResolvedValue(JSON.stringify({
91
- name: '@amodalai/test',
92
- version: '1.0.0',
93
- amodal: {tags: ['skill']},
94
- }));
95
-
96
- const {runPublish} = await import('./publish.js');
97
- const result = await runPublish();
98
- expect(result).toBe(1);
99
- expect(stderrOutput).toContain('name');
100
- });
101
-
102
- it('should handle npm publish failure', async () => {
103
- mockExecFile.mockImplementation(
104
- (_cmd: string, _args: string[], _opts: unknown, cb: (err: Error | null) => void) => {
105
- cb(new Error('Network error'));
106
- return {};
107
- },
108
- );
109
-
110
- const {runPublish} = await import('./publish.js');
111
- const result = await runPublish();
112
- expect(result).toBe(1);
113
- expect(stderrOutput).toContain('Publish failed');
114
- });
115
-
116
- it('should handle already published (409)', async () => {
117
- mockExecFile.mockImplementation(
118
- (_cmd: string, _args: string[], _opts: unknown, cb: (err: Error | null) => void) => {
119
- cb(new Error('EPUBLISHCONFLICT'));
120
- return {};
121
- },
122
- );
123
-
124
- const {runPublish} = await import('./publish.js');
125
- const result = await runPublish();
126
- expect(result).toBe(1);
127
- expect(stderrOutput).toContain('already exists');
128
- });
129
-
130
- it('should suggest npm login on auth error', async () => {
131
- mockExecFile.mockImplementation(
132
- (_cmd: string, _args: string[], _opts: unknown, cb: (err: Error | null) => void) => {
133
- cb(new Error('ENEEDAUTH'));
134
- return {};
135
- },
136
- );
137
-
138
- const {runPublish} = await import('./publish.js');
139
- const result = await runPublish();
140
- expect(result).toBe(1);
141
- expect(stderrOutput).toContain('npm login');
142
- });
143
-
144
- it('should use custom registry', async () => {
145
- mockExecFile.mockImplementation(
146
- (_cmd: string, args: string[], _opts: unknown, cb: (err: Error | null, result: {stdout: string; stderr: string}) => void) => {
147
- expect(args).toContain('https://custom.registry.com');
148
- cb(null, {stdout: '', stderr: ''});
149
- return {};
150
- },
151
- );
152
-
153
- const {runPublish} = await import('./publish.js');
154
- const result = await runPublish({registry: 'https://custom.registry.com'});
155
- expect(result).toBe(0);
156
- });
157
-
158
- it('should show package name and version in dry run', async () => {
159
- mockReadFile.mockResolvedValue(JSON.stringify({
160
- name: '@amodalai/connection-stripe',
161
- version: '3.2.1',
162
- amodal: {name: 'stripe', tags: ['connection']},
163
- }));
164
-
165
- const {runPublish} = await import('./publish.js');
166
- await runPublish({dryRun: true});
167
- expect(stderrOutput).toContain('@amodalai/connection-stripe@3.2.1');
168
- });
169
- });
@@ -1,141 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Amodal Labs, Inc.
4
- * SPDX-License-Identifier: MIT
5
- */
6
-
7
- import {readFile, readdir, stat, writeFile} from 'node:fs/promises';
8
- import {execFile} from 'node:child_process';
9
- import * as path from 'node:path';
10
- import {promisify} from 'node:util';
11
-
12
- import type {CommandModule} from 'yargs';
13
- import {KNOWN_CONTENT_DIRS} from '@amodalai/core';
14
-
15
- const execFileAsync = promisify(execFile);
16
-
17
- const DEFAULT_REGISTRY = 'https://registry.amodalai.com';
18
-
19
- export interface PublishOptions {
20
- cwd?: string;
21
- dryRun?: boolean;
22
- registry?: string;
23
- }
24
-
25
- /**
26
- * Publish a package to the registry.
27
- * Returns 0 on success, 1 on error.
28
- */
29
- export async function runPublish(options: PublishOptions = {}): Promise<number> {
30
- const cwd = options.cwd ?? process.cwd();
31
- const registry = options.registry ?? DEFAULT_REGISTRY;
32
-
33
- // Validate package.json exists
34
- const pkgJsonPath = path.join(cwd, 'package.json');
35
- let pkgName: string;
36
- let pkgVersion: string;
37
- let manifestName: string;
38
-
39
- try {
40
- const content = await readFile(pkgJsonPath, 'utf-8');
41
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- config JSON
42
- const pkg = JSON.parse(content) as Record<string, unknown>;
43
- pkgName = String(pkg['name'] ?? '');
44
- pkgVersion = String(pkg['version'] ?? '');
45
-
46
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
47
- const amodal = pkg['amodal'] as Record<string, unknown> | undefined;
48
- if (!amodal || typeof amodal !== 'object' || !amodal['name']) {
49
- process.stderr.write('[publish] package.json must have an "amodal" block with a "name" field.\n');
50
- return 1;
51
- }
52
- manifestName = String(amodal['name']);
53
- } catch (err) {
54
- const msg = err instanceof Error ? err.message : String(err);
55
- process.stderr.write(`[publish] Failed to read package.json: ${msg}\n`);
56
- return 1;
57
- }
58
-
59
- // Scan for known content directories and auto-populate `contains`
60
- const contains: string[] = [];
61
- for (const dir of KNOWN_CONTENT_DIRS) {
62
- try {
63
- const s = await stat(path.join(cwd, dir));
64
- if (s.isDirectory()) {
65
- const entries = await readdir(path.join(cwd, dir));
66
- if (entries.length > 0) contains.push(dir);
67
- }
68
- } catch {
69
- // Directory doesn't exist
70
- }
71
- }
72
-
73
- if (contains.length === 0) {
74
- process.stderr.write('[publish] Warning: no known amodal directories found (connections, skills, etc.).\n');
75
- }
76
-
77
- // Write `contains` into package.json before publishing
78
- try {
79
- const rawContent = await readFile(pkgJsonPath, 'utf-8');
80
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- config JSON
81
- const pkgObj = JSON.parse(rawContent) as Record<string, unknown>;
82
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
83
- const amodal = pkgObj['amodal'] as Record<string, unknown>;
84
- amodal['contains'] = contains;
85
- await writeFile(pkgJsonPath, JSON.stringify(pkgObj, null, 2) + '\n', 'utf-8');
86
- } catch {
87
- // Non-fatal — publish will proceed without contains
88
- }
89
-
90
- if (options.dryRun) {
91
- process.stderr.write(`[publish] Dry run: would publish ${pkgName}@${pkgVersion}\n`);
92
- process.stderr.write(`[publish] Name: ${manifestName}\n`);
93
- process.stderr.write(`[publish] Contains: ${contains.length > 0 ? contains.join(', ') : '(none)'}\n`);
94
- process.stderr.write(`[publish] Registry: ${registry}\n`);
95
- return 0;
96
- }
97
-
98
- process.stderr.write(`[publish] Publishing ${pkgName}@${pkgVersion} to ${registry}...\n`);
99
-
100
- try {
101
- await execFileAsync(
102
- 'npm',
103
- ['publish', '--registry', registry],
104
- {
105
- cwd,
106
- timeout: 120_000,
107
- },
108
- );
109
-
110
- process.stderr.write(`[publish] Published ${pkgName}@${pkgVersion}\n`);
111
- return 0;
112
- } catch (err) {
113
- const msg = err instanceof Error ? err.message : String(err);
114
- if (msg.includes('403') || msg.includes('409') || msg.includes('EPUBLISHCONFLICT')) {
115
- process.stderr.write(`[publish] Version ${pkgVersion} already exists. Bump the version and try again.\n`);
116
- } else if (msg.includes('ENEEDAUTH') || msg.includes('E401')) {
117
- process.stderr.write(`[publish] Not authenticated. Run \`npm login --registry ${registry}\` first.\n`);
118
- } else {
119
- process.stderr.write(`[publish] Publish failed: ${msg}\n`);
120
- }
121
- return 1;
122
- }
123
- }
124
-
125
- export const publishCommand: CommandModule = {
126
- command: 'publish',
127
- describe: 'Publish a package to the registry',
128
- builder: (yargs) =>
129
- yargs
130
- .option('dry-run', {type: 'boolean', default: false, describe: 'Show what would be published'})
131
- .option('registry', {type: 'string', describe: 'Registry URL'}),
132
- handler: async (argv) => {
133
- const code = await runPublish({
134
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
135
- dryRun: argv['dryRun'] as boolean,
136
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
137
- registry: argv['registry'] as string | undefined,
138
- });
139
- process.exit(code);
140
- },
141
- };