@amodalai/amodal 0.2.0 → 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 +39 -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
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type {CommandModule} from 'yargs';
8
- import {loadRepo, setupSession, readLockFile, resolveAllPackages} from '@amodalai/core';
8
+ import {loadRepo, setupSession, resolveAllPackages} from '@amodalai/core';
9
9
  import {findRepoRoot} from '../shared/repo-discovery.js';
10
10
 
11
11
  export interface InspectOptions {
@@ -79,47 +79,42 @@ export async function runInspect(options: InspectOptions = {}): Promise<void> {
79
79
  if (options.resolved) {
80
80
  process.stdout.write('\n=== Resolved Packages ===\n');
81
81
 
82
- const lockFile = await readLockFile(repoPath);
83
- if (!lockFile) {
84
- process.stdout.write(' No lock file found.\n');
85
- } else {
86
- const resolved = await resolveAllPackages({repoPath, lockFile});
87
-
88
- // Filter by scope if given
89
- const scopeParts = options.scope?.split('/');
90
- const scopeType = scopeParts?.[0];
91
- const scopeName = scopeParts?.[1];
92
-
93
- if (!scopeType || scopeType === 'connections') {
94
- for (const [name, conn] of resolved.connections) {
95
- if (scopeName && name !== scopeName) continue;
96
- process.stdout.write(`\n Connection: ${name}\n`);
97
- process.stdout.write(` Endpoints: ${conn.surface.length}\n`);
98
- process.stdout.write(` Auth: ${conn.spec.auth?.type ?? 'none'}\n`);
99
- }
82
+ const resolved = await resolveAllPackages({repoPath});
83
+
84
+ // Filter by scope if given
85
+ const scopeParts = options.scope?.split('/');
86
+ const scopeType = scopeParts?.[0];
87
+ const scopeName = scopeParts?.[1];
88
+
89
+ if (!scopeType || scopeType === 'connections') {
90
+ for (const [name, conn] of resolved.connections) {
91
+ if (scopeName && name !== scopeName) continue;
92
+ process.stdout.write(`\n Connection: ${name}\n`);
93
+ process.stdout.write(` Endpoints: ${conn.surface.length}\n`);
94
+ process.stdout.write(` Auth: ${conn.spec.auth?.type ?? 'none'}\n`);
100
95
  }
96
+ }
101
97
 
102
- if (!scopeType || scopeType === 'skills') {
103
- for (const skill of resolved.skills) {
104
- if (scopeName && skill.name !== scopeName) continue;
105
- process.stdout.write(`\n Skill: ${skill.name}\n`);
106
- process.stdout.write(` Body: ${skill.body.length} chars\n`);
107
- }
98
+ if (!scopeType || scopeType === 'skills') {
99
+ for (const skill of resolved.skills) {
100
+ if (scopeName && skill.name !== scopeName) continue;
101
+ process.stdout.write(`\n Skill: ${skill.name}\n`);
102
+ process.stdout.write(` Body: ${skill.body.length} chars\n`);
108
103
  }
104
+ }
109
105
 
110
- if (!scopeType || scopeType === 'automations') {
111
- for (const auto of resolved.automations) {
112
- if (scopeName && auto.name !== scopeName) continue;
113
- process.stdout.write(`\n Automation: ${auto.name}\n`);
114
- }
106
+ if (!scopeType || scopeType === 'automations') {
107
+ for (const auto of resolved.automations) {
108
+ if (scopeName && auto.name !== scopeName) continue;
109
+ process.stdout.write(`\n Automation: ${auto.name}\n`);
115
110
  }
111
+ }
116
112
 
117
- // Print warnings
118
- if (resolved.warnings.length > 0) {
119
- process.stdout.write('\n Warnings:\n');
120
- for (const warning of resolved.warnings) {
121
- process.stdout.write(` - ${warning}\n`);
122
- }
113
+ // Print warnings
114
+ if (resolved.warnings.length > 0) {
115
+ process.stdout.write('\n Warnings:\n');
116
+ for (const warning of resolved.warnings) {
117
+ process.stdout.write(` - ${warning}\n`);
123
118
  }
124
119
  }
125
120
  }
@@ -1,43 +1,35 @@
1
1
  /**
2
2
  * @license
3
- * Copyright 2025 Amodal Labs, Inc.
3
+ * Copyright 2026 Amodal Labs, Inc.
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
 
7
7
  import {describe, it, expect, vi, beforeEach} from 'vitest';
8
8
 
9
9
  const mockFindRepoRoot = vi.fn(() => '/test/repo');
10
- const mockEnsureNpmContext = vi.fn();
11
- const mockNpmInstall = vi.fn();
12
- const mockReadLockFile = vi.fn();
13
- const mockReadConfigDeps = vi.fn();
10
+ const mockEnsurePackageJson = vi.fn();
11
+ const mockPmAdd = vi.fn();
12
+ const mockPmInstall = vi.fn();
13
+ const mockAddAmodalPackage = vi.fn();
14
14
  const mockToNpmName = vi.fn((name: string) => `@amodalai/${name}`);
15
- const mockAddConfigDep = vi.fn();
16
- const mockDiscoverInstalledPackages = vi.fn();
17
- const mockBuildLockFile = vi.fn();
15
+
16
+ const mockReadFileSync = vi.fn();
18
17
 
19
18
  vi.mock('../shared/repo-discovery.js', () => ({
20
19
  findRepoRoot: mockFindRepoRoot,
21
20
  }));
22
21
 
23
22
  vi.mock('@amodalai/core', () => ({
24
- ensureNpmContext: mockEnsureNpmContext,
25
- npmInstall: mockNpmInstall,
26
- readLockFile: mockReadLockFile,
27
- readConfigDeps: mockReadConfigDeps,
23
+ ensurePackageJson: mockEnsurePackageJson,
24
+ pmAdd: mockPmAdd,
25
+ pmInstall: mockPmInstall,
26
+ addAmodalPackage: mockAddAmodalPackage,
28
27
  toNpmName: mockToNpmName,
29
- addConfigDep: mockAddConfigDep,
30
- discoverInstalledPackages: mockDiscoverInstalledPackages,
31
- buildLockFile: mockBuildLockFile,
32
28
  }));
33
29
 
34
- const mockPaths = {
35
- root: '/test/repo/.amodal/packages',
36
- npmDir: '/test/repo/.amodal/packages/.npm',
37
- npmrc: '/test/repo/.amodal/packages/.npm/.npmrc',
38
- packageJson: '/test/repo/.amodal/packages/.npm/package.json',
39
- nodeModules: '/test/repo/.amodal/packages/.npm/node_modules',
40
- };
30
+ vi.mock('node:fs', () => ({
31
+ readFileSync: mockReadFileSync,
32
+ }));
41
33
 
42
34
  describe('runInstallPkg', () => {
43
35
  let stderrOutput: string;
@@ -45,11 +37,9 @@ describe('runInstallPkg', () => {
45
37
  beforeEach(() => {
46
38
  vi.clearAllMocks();
47
39
  mockFindRepoRoot.mockReturnValue('/test/repo');
48
- mockEnsureNpmContext.mockResolvedValue(mockPaths);
49
- mockNpmInstall.mockResolvedValue({version: '1.0.0', integrity: 'sha512-abc'});
50
- mockDiscoverInstalledPackages.mockResolvedValue([]);
51
- mockBuildLockFile.mockResolvedValue(undefined);
52
- mockAddConfigDep.mockResolvedValue(undefined);
40
+ mockPmAdd.mockResolvedValue(undefined);
41
+ mockPmInstall.mockResolvedValue(undefined);
42
+ mockReadFileSync.mockReturnValue(JSON.stringify({name: 'test-project'}));
53
43
  stderrOutput = '';
54
44
  vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
55
45
  stderrOutput += String(chunk);
@@ -57,66 +47,48 @@ describe('runInstallPkg', () => {
57
47
  });
58
48
  });
59
49
 
60
- it('should restore from lock file on bare install', async () => {
61
- mockReadLockFile.mockResolvedValue({
62
- lockVersion: 2,
63
- packages: {'@amodalai/connection-salesforce': {version: '2.0.0', integrity: 'sha512-x'}},
64
- });
65
-
50
+ it('should run pmInstall on bare install', async () => {
66
51
  const {runInstallPkg} = await import('./install-pkg.js');
67
52
  const result = await runInstallPkg();
68
53
  expect(result).toBe(0);
69
- expect(mockNpmInstall).toHaveBeenCalledWith(mockPaths, '@amodalai/connection-salesforce', '2.0.0');
70
- expect(stderrOutput).toContain('Restoring 1 package');
54
+ expect(mockEnsurePackageJson).toHaveBeenCalledWith('/test/repo', 'test-project');
55
+ expect(mockPmInstall).toHaveBeenCalledWith('/test/repo');
56
+ expect(stderrOutput).toContain('Done');
71
57
  });
72
58
 
73
- it('should print nothing to install when no lock file and no deps on bare install', async () => {
74
- mockReadLockFile.mockResolvedValue(null);
75
- mockReadConfigDeps.mockResolvedValue({});
59
+ it('should handle pmInstall failure on bare install', async () => {
60
+ mockPmInstall.mockRejectedValue(new Error('Install failed'));
76
61
 
77
62
  const {runInstallPkg} = await import('./install-pkg.js');
78
63
  const result = await runInstallPkg();
79
- expect(result).toBe(0);
80
- expect(stderrOutput).toContain('Nothing to install');
64
+ expect(result).toBe(1);
65
+ expect(stderrOutput).toContain('Install failed');
81
66
  });
82
67
 
83
68
  it('should install a single package', async () => {
84
- mockDiscoverInstalledPackages.mockResolvedValue([
85
- {npmName: '@amodalai/alert-enrichment', version: '1.0.0', integrity: 'sha512-abc'},
86
- ]);
87
-
88
69
  const {runInstallPkg} = await import('./install-pkg.js');
89
70
  const result = await runInstallPkg({
90
71
  packages: ['alert-enrichment'],
91
72
  });
92
73
  expect(result).toBe(0);
93
74
  expect(mockToNpmName).toHaveBeenCalledWith('alert-enrichment');
94
- expect(mockNpmInstall).toHaveBeenCalledWith(mockPaths, '@amodalai/alert-enrichment');
95
- expect(mockDiscoverInstalledPackages).toHaveBeenCalledWith(mockPaths);
96
- expect(mockBuildLockFile).toHaveBeenCalledWith('/test/repo', expect.any(Array));
75
+ expect(mockPmAdd).toHaveBeenCalledWith('/test/repo', '@amodalai/alert-enrichment');
76
+ expect(mockAddAmodalPackage).toHaveBeenCalledWith('/test/repo', '@amodalai/alert-enrichment');
97
77
  });
98
78
 
99
79
  it('should install multiple packages', async () => {
100
- mockDiscoverInstalledPackages.mockResolvedValue([
101
- {npmName: '@amodalai/connection-stripe', version: '1.0.0', integrity: 'sha512-abc'},
102
- {npmName: '@amodalai/soc-agent', version: '1.0.0', integrity: 'sha512-def'},
103
- ]);
104
-
105
80
  const {runInstallPkg} = await import('./install-pkg.js');
106
81
  const result = await runInstallPkg({
107
82
  packages: ['connection-stripe', 'soc-agent'],
108
83
  });
109
84
  expect(result).toBe(0);
110
- expect(mockNpmInstall).toHaveBeenCalledTimes(2);
85
+ expect(mockPmAdd).toHaveBeenCalledTimes(2);
111
86
  });
112
87
 
113
88
  it('should continue on error and report failure count', async () => {
114
- mockNpmInstall
89
+ mockPmAdd
115
90
  .mockRejectedValueOnce(new Error('Network error'))
116
- .mockResolvedValueOnce({version: '1.0.0', integrity: 'sha512-ok'});
117
- mockDiscoverInstalledPackages.mockResolvedValue([
118
- {npmName: '@amodalai/good', version: '1.0.0', integrity: 'sha512-ok'},
119
- ]);
91
+ .mockResolvedValueOnce(undefined);
120
92
 
121
93
  const {runInstallPkg} = await import('./install-pkg.js');
122
94
  const result = await runInstallPkg({
@@ -128,10 +100,9 @@ describe('runInstallPkg', () => {
128
100
  });
129
101
 
130
102
  it('should report all failures', async () => {
131
- mockNpmInstall
103
+ mockPmAdd
132
104
  .mockRejectedValueOnce(new Error('Fail 1'))
133
105
  .mockRejectedValueOnce(new Error('Fail 2'));
134
- mockDiscoverInstalledPackages.mockResolvedValue([]);
135
106
 
136
107
  const {runInstallPkg} = await import('./install-pkg.js');
137
108
  const result = await runInstallPkg({
@@ -152,27 +123,25 @@ describe('runInstallPkg', () => {
152
123
  expect(stderrOutput).toContain('Not found');
153
124
  });
154
125
 
155
- it('should handle bare install with empty lock file packages', async () => {
156
- mockReadLockFile.mockResolvedValue({lockVersion: 2, packages: {}});
126
+ it('should use default project name when amodal.json is missing', async () => {
127
+ mockReadFileSync.mockImplementation(() => {
128
+ throw new Error('ENOENT');
129
+ });
157
130
 
158
131
  const {runInstallPkg} = await import('./install-pkg.js');
159
132
  const result = await runInstallPkg();
160
133
  expect(result).toBe(0);
161
- expect(stderrOutput).toContain('Nothing to install');
134
+ expect(mockEnsurePackageJson).toHaveBeenCalledWith('/test/repo', 'amodal-project');
162
135
  });
163
136
 
164
137
  it('should pass cwd to findRepoRoot', async () => {
165
- mockReadLockFile.mockResolvedValue(null);
166
- mockReadConfigDeps.mockResolvedValue({});
167
-
168
138
  const {runInstallPkg} = await import('./install-pkg.js');
169
139
  await runInstallPkg({cwd: '/custom/dir'});
170
140
  expect(mockFindRepoRoot).toHaveBeenCalledWith('/custom/dir');
171
141
  });
172
142
 
173
- it('should handle npm install failure for single package', async () => {
174
- mockNpmInstall.mockRejectedValue(new Error('Registry unreachable'));
175
- mockDiscoverInstalledPackages.mockResolvedValue([]);
143
+ it('should handle npm add failure for single package', async () => {
144
+ mockPmAdd.mockRejectedValue(new Error('Registry unreachable'));
176
145
 
177
146
  const {runInstallPkg} = await import('./install-pkg.js');
178
147
  const result = await runInstallPkg({
@@ -183,12 +152,9 @@ describe('runInstallPkg', () => {
183
152
  });
184
153
 
185
154
  it('should handle empty packages array like bare install', async () => {
186
- mockReadLockFile.mockResolvedValue(null);
187
- mockReadConfigDeps.mockResolvedValue({});
188
-
189
155
  const {runInstallPkg} = await import('./install-pkg.js');
190
156
  const result = await runInstallPkg({packages: []});
191
157
  expect(result).toBe(0);
192
- expect(stderrOutput).toContain('Nothing to install');
158
+ expect(mockPmInstall).toHaveBeenCalledWith('/test/repo');
193
159
  });
194
160
  });
@@ -1,30 +1,29 @@
1
1
  /**
2
2
  * @license
3
- * Copyright 2025 Amodal Labs, Inc.
3
+ * Copyright 2026 Amodal Labs, Inc.
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
 
7
+ import {readFileSync} from 'node:fs';
8
+ import * as path from 'node:path';
7
9
  import type {CommandModule} from 'yargs';
8
10
  import {
9
- addConfigDep,
10
- buildLockFile,
11
- discoverInstalledPackages,
12
- ensureNpmContext,
13
- npmInstall,
14
- readConfigDeps,
15
- readLockFile,
11
+ ensurePackageJson,
12
+ addAmodalPackage,
13
+ pmAdd,
14
+ pmInstall,
16
15
  toNpmName,
17
16
  } from '@amodalai/core';
18
17
  import {findRepoRoot} from '../shared/repo-discovery.js';
19
18
 
20
19
  /**
21
- * Install one or more packages, or restore all from lock file.
20
+ * Install one or more packages, or run a bare install.
22
21
  * Returns the number of failures (0 = success).
23
22
  *
24
23
  * Usage:
25
- * amodal install — restore from lock file or amodal.json deps
26
- * amodal install alert-enrichment — install @amodalai/alert-enrichment + transitive deps
27
- * amodal install @amodalai/soc-agent — install with full npm name
24
+ * amodal install — run package manager install
25
+ * amodal install alert-enrichment — add @amodalai/alert-enrichment
26
+ * amodal install @amodalai/soc-agent — add with full npm name
28
27
  */
29
28
  export async function runInstallPkg(options: {cwd?: string; packages?: string[]} = {}): Promise<number> {
30
29
  let repoPath: string;
@@ -36,74 +35,43 @@ export async function runInstallPkg(options: {cwd?: string; packages?: string[]}
36
35
  return 1;
37
36
  }
38
37
 
39
- const paths = await ensureNpmContext(repoPath);
40
-
41
- // Bare install: restore from lock file or amodal.json dependencies
42
- if (!options.packages || options.packages.length === 0) {
43
- const lockFile = await readLockFile(repoPath);
44
-
45
- if (lockFile && Object.keys(lockFile.packages).length > 0) {
46
- const entries = Object.entries(lockFile.packages);
47
- process.stderr.write(`[install] Restoring ${entries.length} package(s) from lock file...\n`);
48
- let failures = 0;
49
- for (const [npmName, entry] of entries) {
50
- try {
51
- await npmInstall(paths, npmName, entry.version);
52
- } catch (err) {
53
- const msg = err instanceof Error ? err.message : String(err);
54
- process.stderr.write(`[install] Failed: ${npmName}@${entry.version}: ${msg}\n`);
55
- failures++;
56
- }
57
- }
58
- if (failures > 0) {
59
- process.stderr.write(`[install] ${failures} package(s) failed.\n`);
60
- return failures;
61
- }
62
- process.stderr.write(`[install] Restored ${entries.length} package(s).\n`);
63
- return 0;
38
+ // Read project name from amodal.json for ensurePackageJson
39
+ let projectName = 'amodal-project';
40
+ try {
41
+ const configRaw = readFileSync(path.join(repoPath, 'amodal.json'), 'utf-8');
42
+ const config: unknown = JSON.parse(configRaw);
43
+ if (config && typeof config === 'object' && 'name' in config) {
44
+ projectName = String((config as Record<string, unknown>)['name']);
64
45
  }
46
+ } catch {
47
+ // Use default project name
48
+ }
65
49
 
66
- // No lock file — check amodal.json dependencies
67
- let deps: Record<string, string>;
68
- try {
69
- deps = await readConfigDeps(repoPath);
70
- } catch {
71
- deps = {};
72
- }
50
+ ensurePackageJson(repoPath, projectName);
73
51
 
74
- if (Object.keys(deps).length === 0) {
75
- process.stderr.write('[install] Nothing to install.\n');
52
+ // Bare install: run package manager install
53
+ if (!options.packages || options.packages.length === 0) {
54
+ process.stderr.write('[install] Running package manager install...\n');
55
+ try {
56
+ await pmInstall(repoPath);
57
+ process.stderr.write('[install] Done.\n');
76
58
  return 0;
59
+ } catch (err) {
60
+ const msg = err instanceof Error ? err.message : String(err);
61
+ process.stderr.write(`[install] Install failed: ${msg}\n`);
62
+ return 1;
77
63
  }
78
-
79
- process.stderr.write(`[install] Installing ${Object.keys(deps).length} package(s) from amodal.json...\n`);
80
- let failures = 0;
81
- for (const [name, versionRange] of Object.entries(deps)) {
82
- const npmName = toNpmName(name);
83
- try {
84
- await npmInstall(paths, npmName, versionRange);
85
- } catch (err) {
86
- const msg = err instanceof Error ? err.message : String(err);
87
- process.stderr.write(`[install] Failed: ${npmName}@${versionRange}: ${msg}\n`);
88
- failures++;
89
- }
90
- }
91
-
92
- // Rebuild lock file from what's actually installed
93
- const discovered = await discoverInstalledPackages(paths);
94
- await buildLockFile(repoPath, discovered);
95
- process.stderr.write(`[install] ${discovered.length} package(s) installed.\n`);
96
- return failures;
97
64
  }
98
65
 
99
66
  // Install specific packages
100
67
  let failures = 0;
101
68
  for (const name of options.packages) {
102
69
  const npmName = toNpmName(name);
103
- process.stderr.write(`[install] Installing ${npmName}...\n`);
70
+ process.stderr.write(`[install] Adding ${npmName}...\n`);
104
71
  try {
105
- await npmInstall(paths, npmName);
106
- process.stderr.write(`[install] Installed ${npmName}\n`);
72
+ await pmAdd(repoPath, npmName);
73
+ addAmodalPackage(repoPath, npmName);
74
+ process.stderr.write(`[install] Added ${npmName}\n`);
107
75
  } catch (err) {
108
76
  const msg = err instanceof Error ? err.message : String(err);
109
77
  process.stderr.write(`[install] Failed: ${npmName}: ${msg}\n`);
@@ -111,27 +79,10 @@ export async function runInstallPkg(options: {cwd?: string; packages?: string[]}
111
79
  }
112
80
  }
113
81
 
114
- // Discover everything installed (including transitive deps) and rebuild lock file
115
- const discovered = await discoverInstalledPackages(paths);
116
- await buildLockFile(repoPath, discovered);
117
-
118
- // Add direct installs to amodal.json dependencies
119
- for (const name of options.packages) {
120
- const npmName = toNpmName(name);
121
- const installed = discovered.find((d) => d.npmName === npmName);
122
- if (installed) {
123
- try {
124
- await addConfigDep(repoPath, npmName, installed.version);
125
- } catch {
126
- // amodal.json might not exist yet
127
- }
128
- }
129
- }
130
-
131
82
  if (failures > 0) {
132
83
  process.stderr.write(`[install] ${failures} package(s) failed.\n`);
133
84
  } else {
134
- process.stderr.write(`[install] ${discovered.length} total package(s) (including dependencies).\n`);
85
+ process.stderr.write(`[install] ${options.packages.length} package(s) added.\n`);
135
86
  }
136
87
 
137
88
  return failures;
@@ -139,7 +90,7 @@ export async function runInstallPkg(options: {cwd?: string; packages?: string[]}
139
90
 
140
91
  export const installPkgCommand: CommandModule = {
141
92
  command: 'install [packages..]',
142
- describe: 'Install packages (or restore all from lock file)',
93
+ describe: 'Install packages (or run bare install)',
143
94
  builder: (yargs) =>
144
95
  yargs.positional('packages', {type: 'string', array: true, describe: 'Package names to install'}),
145
96
  handler: async (argv) => {
@@ -1,60 +1,33 @@
1
1
  /**
2
2
  * @license
3
- * Copyright 2025 Amodal Labs, Inc.
3
+ * Copyright 2026 Amodal Labs, Inc.
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
 
7
7
  import {describe, it, expect, vi, beforeEach} from 'vitest';
8
8
 
9
9
  const mockFindRepoRoot = vi.fn(() => '/test/repo');
10
- const mockGetNpmContextPaths = vi.fn();
11
10
  const mockToNpmName = vi.fn((name: string) => `@amodalai/${name}`);
12
- const mockNpmUninstall = vi.fn();
13
- const mockEnsureNpmContext = vi.fn();
14
- const mockDiscoverInstalledPackages = vi.fn();
15
- const mockBuildLockFile = vi.fn();
16
-
17
- const mockReadFile = vi.fn();
18
- const mockWriteFile = vi.fn();
11
+ const mockPmRemove = vi.fn();
12
+ const mockRemoveAmodalPackage = vi.fn();
19
13
 
20
14
  vi.mock('../shared/repo-discovery.js', () => ({
21
15
  findRepoRoot: mockFindRepoRoot,
22
16
  }));
23
17
 
24
18
  vi.mock('@amodalai/core', () => ({
25
- getNpmContextPaths: mockGetNpmContextPaths,
26
19
  toNpmName: mockToNpmName,
27
- npmUninstall: mockNpmUninstall,
28
- ensureNpmContext: mockEnsureNpmContext,
29
- discoverInstalledPackages: mockDiscoverInstalledPackages,
30
- buildLockFile: mockBuildLockFile,
31
- }));
32
-
33
- vi.mock('node:fs/promises', () => ({
34
- readFile: mockReadFile,
35
- writeFile: mockWriteFile,
20
+ pmRemove: mockPmRemove,
21
+ removeAmodalPackage: mockRemoveAmodalPackage,
36
22
  }));
37
23
 
38
- const mockPaths = {
39
- root: '/test/repo/.amodal/packages',
40
- npmDir: '/test/repo/.amodal/packages/.npm',
41
- npmrc: '/test/repo/.amodal/packages/.npm/.npmrc',
42
- packageJson: '/test/repo/.amodal/packages/.npm/package.json',
43
- nodeModules: '/test/repo/.amodal/packages/.npm/node_modules',
44
- };
45
-
46
24
  describe('runUninstall', () => {
47
25
  let stderrOutput: string;
48
26
 
49
27
  beforeEach(() => {
50
28
  vi.clearAllMocks();
51
29
  mockFindRepoRoot.mockReturnValue('/test/repo');
52
- mockGetNpmContextPaths.mockReturnValue(mockPaths);
53
- mockNpmUninstall.mockResolvedValue(undefined);
54
- mockEnsureNpmContext.mockResolvedValue(mockPaths);
55
- mockDiscoverInstalledPackages.mockResolvedValue([]);
56
- mockBuildLockFile.mockResolvedValue(undefined);
57
- mockReadFile.mockRejectedValue(new Error('ENOENT')); // amodal.json not found by default
30
+ mockPmRemove.mockResolvedValue(undefined);
58
31
  stderrOutput = '';
59
32
  vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
60
33
  stderrOutput += String(chunk);
@@ -66,19 +39,18 @@ describe('runUninstall', () => {
66
39
  const {runUninstall} = await import('./uninstall.js');
67
40
  const result = await runUninstall({name: 'connection-stripe'});
68
41
  expect(result).toBe(0);
69
- expect(mockNpmUninstall).toHaveBeenCalledWith(mockPaths, '@amodalai/connection-stripe');
70
- expect(mockDiscoverInstalledPackages).toHaveBeenCalledWith(mockPaths);
71
- expect(mockBuildLockFile).toHaveBeenCalledWith('/test/repo', []);
42
+ expect(mockPmRemove).toHaveBeenCalledWith('/test/repo', '@amodalai/connection-stripe');
43
+ expect(mockRemoveAmodalPackage).toHaveBeenCalledWith('/test/repo', '@amodalai/connection-stripe');
72
44
  expect(stderrOutput).toContain('Removed @amodalai/connection-stripe');
73
45
  });
74
46
 
75
- it('should return 1 when npm uninstall fails', async () => {
76
- mockNpmUninstall.mockRejectedValue(new Error('npm error'));
47
+ it('should return 1 when pm remove fails', async () => {
48
+ mockPmRemove.mockRejectedValue(new Error('npm error'));
77
49
 
78
50
  const {runUninstall} = await import('./uninstall.js');
79
51
  const result = await runUninstall({name: 'skill-triage'});
80
52
  expect(result).toBe(1);
81
- expect(stderrOutput).toContain('npm uninstall failed');
53
+ expect(stderrOutput).toContain('Failed to remove');
82
54
  });
83
55
 
84
56
  it('should return 1 when repo not found', async () => {
@@ -97,16 +69,4 @@ describe('runUninstall', () => {
97
69
  await runUninstall({cwd: '/custom/dir', name: 'connection-test'});
98
70
  expect(mockFindRepoRoot).toHaveBeenCalledWith('/custom/dir');
99
71
  });
100
-
101
- it('should remove dependency from amodal.json when it exists', async () => {
102
- mockReadFile.mockResolvedValue(JSON.stringify({
103
- name: 'test',
104
- dependencies: {'@amodalai/connection-stripe': '1.0.0', '@amodalai/soc-agent': '2.0.0'},
105
- }));
106
-
107
- const {runUninstall} = await import('./uninstall.js');
108
- const result = await runUninstall({name: 'connection-stripe'});
109
- expect(result).toBe(0);
110
- expect(mockWriteFile).toHaveBeenCalled();
111
- });
112
72
  });
@@ -1,24 +1,19 @@
1
1
  /**
2
2
  * @license
3
- * Copyright 2025 Amodal Labs, Inc.
3
+ * Copyright 2026 Amodal Labs, Inc.
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
 
7
- import {readFile, writeFile} from 'node:fs/promises';
8
- import * as path from 'node:path';
9
7
  import type {CommandModule} from 'yargs';
10
8
  import {
11
- buildLockFile,
12
- discoverInstalledPackages,
13
- ensureNpmContext,
14
- getNpmContextPaths,
15
- npmUninstall,
9
+ pmRemove,
10
+ removeAmodalPackage,
16
11
  toNpmName,
17
12
  } from '@amodalai/core';
18
13
  import {findRepoRoot} from '../shared/repo-discovery.js';
19
14
 
20
15
  /**
21
- * Uninstall a package: npm uninstall + rebuild lock file.
16
+ * Uninstall a package via the detected package manager.
22
17
  * Returns 0 on success, 1 on error.
23
18
  */
24
19
  export async function runUninstall(options: {cwd?: string; name: string}): Promise<number> {
@@ -32,40 +27,16 @@ export async function runUninstall(options: {cwd?: string; name: string}): Promi
32
27
  }
33
28
 
34
29
  const npmName = toNpmName(options.name);
35
- const paths = getNpmContextPaths(repoPath);
36
30
 
37
31
  try {
38
- await npmUninstall(paths, npmName);
32
+ await pmRemove(repoPath, npmName);
33
+ removeAmodalPackage(repoPath, npmName);
39
34
  } catch (err) {
40
35
  const msg = err instanceof Error ? err.message : String(err);
41
- process.stderr.write(`[uninstall] npm uninstall failed: ${msg}\n`);
36
+ process.stderr.write(`[uninstall] Failed to remove ${npmName}: ${msg}\n`);
42
37
  return 1;
43
38
  }
44
39
 
45
- // Rebuild lock file from remaining packages
46
- await ensureNpmContext(repoPath);
47
- const discovered = await discoverInstalledPackages(paths);
48
- await buildLockFile(repoPath, discovered);
49
-
50
- // Remove from amodal.json dependencies
51
- try {
52
- const configPath = path.join(repoPath, 'amodal.json');
53
- const configRaw = await readFile(configPath, 'utf-8');
54
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- config JSON
55
- const config = JSON.parse(configRaw) as Record<string, unknown>;
56
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
57
- const deps = (config['dependencies'] as Record<string, string>) ?? {};
58
- delete deps[npmName];
59
- if (Object.keys(deps).length > 0) {
60
- config['dependencies'] = deps;
61
- } else {
62
- delete config['dependencies'];
63
- }
64
- await writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
65
- } catch {
66
- // Non-fatal
67
- }
68
-
69
40
  process.stderr.write(`[uninstall] Removed ${npmName}\n`);
70
41
  return 0;
71
42
  }