@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.
- package/CHANGELOG.md +29 -0
- package/dist/src/commands/connect-channel.d.ts +11 -0
- package/dist/src/commands/connect-channel.d.ts.map +1 -0
- package/dist/src/commands/connect-channel.js +169 -0
- package/dist/src/commands/connect-channel.js.map +1 -0
- package/dist/src/commands/connect.d.ts +3 -1
- package/dist/src/commands/connect.d.ts.map +1 -1
- package/dist/src/commands/connect.js +27 -16
- package/dist/src/commands/connect.js.map +1 -1
- package/dist/src/commands/groups/connect.d.ts +8 -0
- package/dist/src/commands/groups/connect.d.ts.map +1 -0
- package/dist/src/commands/groups/connect.js +17 -0
- package/dist/src/commands/groups/connect.js.map +1 -0
- package/dist/src/commands/groups/pkg.d.ts.map +1 -1
- package/dist/src/commands/groups/pkg.js +0 -12
- package/dist/src/commands/groups/pkg.js.map +1 -1
- package/dist/src/commands/index.d.ts +1 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +3 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +4 -1
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/inspect.d.ts.map +1 -1
- package/dist/src/commands/inspect.js +32 -38
- package/dist/src/commands/inspect.js.map +1 -1
- package/dist/src/commands/install-pkg.d.ts +5 -5
- package/dist/src/commands/install-pkg.d.ts.map +1 -1
- package/dist/src/commands/install-pkg.js +35 -77
- package/dist/src/commands/install-pkg.js.map +1 -1
- package/dist/src/commands/uninstall.d.ts +2 -2
- package/dist/src/commands/uninstall.d.ts.map +1 -1
- package/dist/src/commands/uninstall.js +6 -32
- package/dist/src/commands/uninstall.js.map +1 -1
- package/dist/src/commands/validate.d.ts.map +1 -1
- package/dist/src/commands/validate.js +15 -21
- package/dist/src/commands/validate.js.map +1 -1
- package/dist/src/shared/tarball.js +1 -1
- package/dist/src/shared/tarball.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/commands/command-exports.test.ts +1 -1
- package/src/commands/connect-channel.ts +197 -0
- package/src/commands/connect.test.ts +28 -40
- package/src/commands/connect.ts +30 -24
- package/src/commands/groups/connect.ts +20 -0
- package/src/commands/groups/pkg.ts +0 -12
- package/src/commands/index.ts +5 -1
- package/src/commands/init.test.ts +2 -2
- package/src/commands/init.ts +5 -1
- package/src/commands/inspect.test.ts +1 -18
- package/src/commands/inspect.ts +31 -36
- package/src/commands/install-pkg.test.ts +39 -73
- package/src/commands/install-pkg.ts +38 -87
- package/src/commands/uninstall.test.ts +11 -51
- package/src/commands/uninstall.ts +7 -36
- package/src/commands/validate.test.ts +5 -26
- package/src/commands/validate.ts +15 -20
- package/src/e2e-commands.test.ts +0 -1
- package/src/e2e-plugins.test.ts +0 -1
- package/src/e2e.test.ts +0 -89
- package/src/shared/tarball.ts +1 -1
- package/dist/src/commands/diff.d.ts +0 -17
- package/dist/src/commands/diff.d.ts.map +0 -1
- package/dist/src/commands/diff.js +0 -118
- package/dist/src/commands/diff.js.map +0 -1
- package/dist/src/commands/list.d.ts +0 -18
- package/dist/src/commands/list.d.ts.map +0 -1
- package/dist/src/commands/list.js +0 -82
- package/dist/src/commands/list.js.map +0 -1
- package/dist/src/commands/publish.d.ts +0 -18
- package/dist/src/commands/publish.d.ts.map +0 -1
- package/dist/src/commands/publish.js +0 -121
- package/dist/src/commands/publish.js.map +0 -1
- package/dist/src/commands/search.d.ts +0 -19
- package/dist/src/commands/search.d.ts.map +0 -1
- package/dist/src/commands/search.js +0 -97
- package/dist/src/commands/search.js.map +0 -1
- package/dist/src/commands/update.d.ts +0 -19
- package/dist/src/commands/update.d.ts.map +0 -1
- package/dist/src/commands/update.js +0 -158
- package/dist/src/commands/update.js.map +0 -1
- package/src/commands/diff.test.ts +0 -165
- package/src/commands/diff.ts +0 -141
- package/src/commands/list.test.ts +0 -141
- package/src/commands/list.ts +0 -99
- package/src/commands/publish.test.ts +0 -169
- package/src/commands/publish.ts +0 -141
- package/src/commands/search.test.ts +0 -171
- package/src/commands/search.ts +0 -120
- package/src/commands/update.test.ts +0 -256
- package/src/commands/update.ts +0 -196
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amodalai/amodal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Amodal CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"semver": "^7.6.0",
|
|
31
31
|
"yargs": "^17.7.2",
|
|
32
32
|
"zod": "^4.3.6",
|
|
33
|
-
"@amodalai/
|
|
34
|
-
"@amodalai/
|
|
35
|
-
"@amodalai/runtime": "0.2.
|
|
36
|
-
"@amodalai/runtime-app": "0.2.
|
|
33
|
+
"@amodalai/types": "0.2.2",
|
|
34
|
+
"@amodalai/core": "0.2.2",
|
|
35
|
+
"@amodalai/runtime": "0.2.2",
|
|
36
|
+
"@amodalai/runtime-app": "0.2.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^20.11.24",
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `amodal connect channel <name>` — install a channel package and run
|
|
9
|
+
* its interactive setup flow (prompt for credentials, set webhooks, etc.).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import {findRepoRoot} from '../shared/repo-discovery.js';
|
|
14
|
+
import type {CommandModule} from 'yargs';
|
|
15
|
+
import {readFileSync, writeFileSync, existsSync} from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import {pathToFileURL} from 'node:url';
|
|
18
|
+
import prompts from 'prompts';
|
|
19
|
+
import {ensurePackageJson, pmAdd, toNpmName} from '@amodalai/core';
|
|
20
|
+
import type {ChannelPlugin, ChannelSetupContext} from '@amodalai/types';
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- yargs CommandModule default generic
|
|
23
|
+
export const connectChannelCommand: CommandModule<{}, {name: string; 'webhook-url'?: string}> = {
|
|
24
|
+
command: 'channel <name>',
|
|
25
|
+
describe: 'Connect a messaging channel (install + setup)',
|
|
26
|
+
builder: (yargs) =>
|
|
27
|
+
yargs
|
|
28
|
+
.positional('name', {type: 'string', demandOption: true, describe: 'Channel package name or short name'})
|
|
29
|
+
.option('webhook-url', {type: 'string', describe: 'Public webhook URL for this channel'}),
|
|
30
|
+
handler: async (argv) => {
|
|
31
|
+
const code = await runConnectChannel({name: argv.name, webhookUrl: argv['webhook-url']});
|
|
32
|
+
process.exit(code);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
interface ConnectChannelOptions {
|
|
37
|
+
name: string;
|
|
38
|
+
cwd?: string;
|
|
39
|
+
webhookUrl?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function runConnectChannel(options: ConnectChannelOptions): Promise<number> {
|
|
43
|
+
let repoPath: string;
|
|
44
|
+
try {
|
|
45
|
+
repoPath = findRepoRoot(options.cwd);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
48
|
+
process.stderr.write(`[connect channel] ${msg}\n`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const npmName = toNpmName(options.name);
|
|
53
|
+
|
|
54
|
+
// Step 1: Install if not present
|
|
55
|
+
const packageDir = path.join(repoPath, 'node_modules', ...npmName.split('/'));
|
|
56
|
+
let alreadyInstalled = false;
|
|
57
|
+
try {
|
|
58
|
+
alreadyInstalled = existsSync(path.join(packageDir, 'package.json'));
|
|
59
|
+
} catch {
|
|
60
|
+
// Not installed
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!alreadyInstalled) {
|
|
64
|
+
process.stderr.write(`[connect channel] Installing ${npmName}...\n`);
|
|
65
|
+
try {
|
|
66
|
+
ensurePackageJson(repoPath, 'amodal-project');
|
|
67
|
+
await pmAdd(repoPath, npmName);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
70
|
+
process.stderr.write(`[connect channel] Install failed: ${msg}\n`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
process.stderr.write(`[connect channel] ${npmName} already installed.\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Step 2: Load the plugin
|
|
78
|
+
const plugin = await loadChannelPlugin(packageDir, npmName);
|
|
79
|
+
if (!plugin) return 1;
|
|
80
|
+
|
|
81
|
+
// Step 3: Add to packages array in amodal.json if not present
|
|
82
|
+
const configPath = path.join(repoPath, 'amodal.json');
|
|
83
|
+
if (!existsSync(configPath)) {
|
|
84
|
+
process.stderr.write('[connect channel] No amodal.json found. Run `amodal init` first.\n');
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- parsing external JSON
|
|
89
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8')) as Record<string, unknown>;
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- config.packages is string[]
|
|
91
|
+
const packages = (config['packages'] ?? []) as string[];
|
|
92
|
+
if (!packages.includes(npmName)) {
|
|
93
|
+
packages.push(npmName);
|
|
94
|
+
config['packages'] = packages;
|
|
95
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
96
|
+
process.stderr.write(`[connect channel] Added ${npmName} to amodal.json packages.\n`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Step 4: Run plugin setup if available
|
|
100
|
+
if (!plugin.setup) {
|
|
101
|
+
process.stderr.write(`[connect channel] ${plugin.channelType} connected. No interactive setup available.\n`);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const context = buildSetupContext(repoPath, configPath, config, options.webhookUrl);
|
|
106
|
+
try {
|
|
107
|
+
await plugin.setup(context);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
110
|
+
process.stderr.write(`[connect channel] Setup failed: ${msg}\n`);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
process.stderr.write(`\n✅ Channel "${plugin.channelType}" connected and configured.\n`);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function loadChannelPlugin(
|
|
119
|
+
packageDir: string,
|
|
120
|
+
npmName: string,
|
|
121
|
+
): Promise<ChannelPlugin | null> {
|
|
122
|
+
const pkgJsonPath = path.join(packageDir, 'package.json');
|
|
123
|
+
if (!existsSync(pkgJsonPath)) {
|
|
124
|
+
process.stderr.write(`[connect channel] Package "${npmName}" not found in node_modules.\n`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- parsing external JSON
|
|
129
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as Record<string, unknown>;
|
|
130
|
+
const mainField = String(pkgJson['main'] ?? 'dist/index.js');
|
|
131
|
+
const entryPath = path.resolve(packageDir, mainField);
|
|
132
|
+
|
|
133
|
+
if (!entryPath.startsWith(path.resolve(packageDir))) {
|
|
134
|
+
process.stderr.write(`[connect channel] Package "${npmName}" has invalid main field.\n`);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- dynamic import
|
|
140
|
+
const mod = await import(pathToFileURL(entryPath).href) as {default?: unknown};
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- validating shape below
|
|
142
|
+
const plugin = mod.default as ChannelPlugin | undefined;
|
|
143
|
+
|
|
144
|
+
if (!plugin || typeof plugin.channelType !== 'string' || typeof plugin.createAdapter !== 'function') {
|
|
145
|
+
process.stderr.write(`[connect channel] Package "${npmName}" does not export a valid ChannelPlugin.\n`);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return plugin;
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
process.stderr.write(`[connect channel] Failed to load plugin: ${msg}\n`);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildSetupContext(
|
|
157
|
+
repoPath: string,
|
|
158
|
+
configPath: string,
|
|
159
|
+
config: Record<string, unknown>,
|
|
160
|
+
webhookUrl?: string,
|
|
161
|
+
): ChannelSetupContext {
|
|
162
|
+
return {
|
|
163
|
+
repoPath,
|
|
164
|
+
config,
|
|
165
|
+
webhookUrl,
|
|
166
|
+
writeEnv: async (key: string, value: string) => {
|
|
167
|
+
const envPath = path.join(repoPath, '.env');
|
|
168
|
+
const existing = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
|
|
169
|
+
const lines = existing.split('\n');
|
|
170
|
+
const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
|
|
171
|
+
if (idx >= 0) {
|
|
172
|
+
lines[idx] = `${key}=${value}`;
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(`${key}=${value}`);
|
|
175
|
+
}
|
|
176
|
+
writeFileSync(envPath, lines.join('\n'));
|
|
177
|
+
},
|
|
178
|
+
updateConfig: async (patch: Record<string, unknown>) => {
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- parsing project config
|
|
180
|
+
const current = JSON.parse(readFileSync(configPath, 'utf-8')) as Record<string, unknown>;
|
|
181
|
+
Object.assign(current, patch);
|
|
182
|
+
writeFileSync(configPath, JSON.stringify(current, null, 2) + '\n');
|
|
183
|
+
},
|
|
184
|
+
prompt: async (message: string, options?: {secret?: boolean; default?: string}) => {
|
|
185
|
+
const response = await prompts({
|
|
186
|
+
type: options?.secret ? 'password' : 'text',
|
|
187
|
+
name: 'value',
|
|
188
|
+
message,
|
|
189
|
+
initial: options?.default,
|
|
190
|
+
});
|
|
191
|
+
if (response.value === undefined) {
|
|
192
|
+
throw new Error('Setup cancelled by user');
|
|
193
|
+
}
|
|
194
|
+
return String(response.value);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
|
-
* Copyright
|
|
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
|
|
11
|
-
const
|
|
12
|
-
const mockAddLockEntry = vi.fn();
|
|
13
|
-
const mockGetLockEntry = vi.fn();
|
|
14
|
-
const mockGetNpmContextPaths = vi.fn();
|
|
10
|
+
const mockEnsurePackageJson = vi.fn();
|
|
11
|
+
const mockPmAdd = vi.fn();
|
|
15
12
|
const mockReadPackageManifest = vi.fn();
|
|
16
13
|
const mockToNpmName = vi.fn((name: string) => `@amodalai/connection-${name}`);
|
|
17
14
|
const mockFindMissingEnvVars = vi.fn();
|
|
18
15
|
const mockUpsertEnvEntries = vi.fn();
|
|
19
|
-
const mockAddConfigDep = vi.fn();
|
|
20
|
-
const mockDiscoverInstalledPackages = vi.fn();
|
|
21
|
-
const mockBuildLockFile = vi.fn();
|
|
22
16
|
|
|
23
17
|
const mockPromptForCredentials = vi.fn();
|
|
24
18
|
const mockTestConnection = vi.fn();
|
|
@@ -26,23 +20,19 @@ const mockRunOAuth2Flow = vi.fn();
|
|
|
26
20
|
|
|
27
21
|
const mockPrompts = vi.fn();
|
|
28
22
|
|
|
23
|
+
const mockStatSync = vi.fn();
|
|
24
|
+
|
|
29
25
|
vi.mock('../shared/repo-discovery.js', () => ({
|
|
30
26
|
findRepoRoot: mockFindRepoRoot,
|
|
31
27
|
}));
|
|
32
28
|
|
|
33
29
|
vi.mock('@amodalai/core', () => ({
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
addLockEntry: mockAddLockEntry,
|
|
37
|
-
getLockEntry: mockGetLockEntry,
|
|
38
|
-
getNpmContextPaths: mockGetNpmContextPaths,
|
|
30
|
+
ensurePackageJson: mockEnsurePackageJson,
|
|
31
|
+
pmAdd: mockPmAdd,
|
|
39
32
|
readPackageManifest: mockReadPackageManifest,
|
|
40
33
|
toNpmName: mockToNpmName,
|
|
41
34
|
findMissingEnvVars: mockFindMissingEnvVars,
|
|
42
35
|
upsertEnvEntries: mockUpsertEnvEntries,
|
|
43
|
-
addConfigDep: mockAddConfigDep,
|
|
44
|
-
discoverInstalledPackages: mockDiscoverInstalledPackages,
|
|
45
|
-
buildLockFile: mockBuildLockFile,
|
|
46
36
|
}));
|
|
47
37
|
|
|
48
38
|
vi.mock('../auth/index.js', () => ({
|
|
@@ -55,13 +45,13 @@ vi.mock('prompts', () => ({
|
|
|
55
45
|
default: mockPrompts,
|
|
56
46
|
}));
|
|
57
47
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
};
|
|
48
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
49
|
+
const actual = await importOriginal<typeof import('node:fs')>();
|
|
50
|
+
return {
|
|
51
|
+
...actual,
|
|
52
|
+
statSync: mockStatSync,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
65
55
|
|
|
66
56
|
const bearerManifest = {
|
|
67
57
|
name: 'stripe',
|
|
@@ -105,16 +95,13 @@ describe('runConnect', () => {
|
|
|
105
95
|
beforeEach(() => {
|
|
106
96
|
vi.clearAllMocks();
|
|
107
97
|
mockFindRepoRoot.mockReturnValue('/test/repo');
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
// By default, package is NOT installed (statSync throws)
|
|
99
|
+
mockStatSync.mockImplementation(() => {
|
|
100
|
+
throw new Error('ENOENT');
|
|
101
|
+
});
|
|
102
|
+
mockPmAdd.mockResolvedValue(undefined);
|
|
113
103
|
mockFindMissingEnvVars.mockResolvedValue([]);
|
|
114
104
|
mockUpsertEnvEntries.mockResolvedValue(undefined);
|
|
115
|
-
mockAddConfigDep.mockResolvedValue(undefined);
|
|
116
|
-
mockDiscoverInstalledPackages.mockResolvedValue([]);
|
|
117
|
-
mockBuildLockFile.mockResolvedValue(undefined);
|
|
118
105
|
mockPromptForCredentials.mockResolvedValue({credentials: {}, summary: 'Set 1 credential'});
|
|
119
106
|
mockTestConnection.mockResolvedValue({connectionName: 'test', results: [], allPassed: true});
|
|
120
107
|
stderrOutput = '';
|
|
@@ -135,7 +122,7 @@ describe('runConnect', () => {
|
|
|
135
122
|
const {runConnect} = await import('./connect.js');
|
|
136
123
|
const result = await runConnect({name: 'stripe'});
|
|
137
124
|
expect(result).toBe(0);
|
|
138
|
-
expect(
|
|
125
|
+
expect(mockPmAdd).toHaveBeenCalled();
|
|
139
126
|
expect(mockPromptForCredentials).toHaveBeenCalled();
|
|
140
127
|
expect(mockTestConnection).toHaveBeenCalled();
|
|
141
128
|
expect(stderrOutput).toContain('Connected: stripe');
|
|
@@ -147,7 +134,7 @@ describe('runConnect', () => {
|
|
|
147
134
|
const {runConnect} = await import('./connect.js');
|
|
148
135
|
const result = await runConnect({name: 'datadog'});
|
|
149
136
|
expect(result).toBe(0);
|
|
150
|
-
expect(
|
|
137
|
+
expect(mockPmAdd).toHaveBeenCalled();
|
|
151
138
|
expect(mockPromptForCredentials).toHaveBeenCalled();
|
|
152
139
|
});
|
|
153
140
|
|
|
@@ -182,18 +169,19 @@ describe('runConnect', () => {
|
|
|
182
169
|
});
|
|
183
170
|
|
|
184
171
|
it('should skip install on reconnect', async () => {
|
|
185
|
-
|
|
172
|
+
// Package already installed in node_modules
|
|
173
|
+
mockStatSync.mockReturnValue({isDirectory: () => true});
|
|
186
174
|
mockReadPackageManifest.mockResolvedValue(bearerManifest);
|
|
187
175
|
|
|
188
176
|
const {runConnect} = await import('./connect.js');
|
|
189
177
|
const result = await runConnect({name: 'stripe'});
|
|
190
178
|
expect(result).toBe(0);
|
|
191
|
-
expect(
|
|
179
|
+
expect(mockPmAdd).not.toHaveBeenCalled();
|
|
192
180
|
expect(stderrOutput).toContain('already installed');
|
|
193
181
|
});
|
|
194
182
|
|
|
195
183
|
it('should skip credential prompt on reconnect when vars present', async () => {
|
|
196
|
-
|
|
184
|
+
mockStatSync.mockReturnValue({isDirectory: () => true});
|
|
197
185
|
mockReadPackageManifest.mockResolvedValue(bearerManifest);
|
|
198
186
|
mockFindMissingEnvVars.mockResolvedValue([]);
|
|
199
187
|
|
|
@@ -204,7 +192,7 @@ describe('runConnect', () => {
|
|
|
204
192
|
});
|
|
205
193
|
|
|
206
194
|
it('should re-prompt on reconnect with force', async () => {
|
|
207
|
-
|
|
195
|
+
mockStatSync.mockReturnValue({isDirectory: () => true});
|
|
208
196
|
mockReadPackageManifest.mockResolvedValue(bearerManifest);
|
|
209
197
|
|
|
210
198
|
const {runConnect} = await import('./connect.js');
|
|
@@ -239,7 +227,7 @@ describe('runConnect', () => {
|
|
|
239
227
|
});
|
|
240
228
|
|
|
241
229
|
it('should return 1 when npm install fails', async () => {
|
|
242
|
-
|
|
230
|
+
mockPmAdd.mockRejectedValue(new Error('Registry down'));
|
|
243
231
|
|
|
244
232
|
const {runConnect} = await import('./connect.js');
|
|
245
233
|
const result = await runConnect({name: 'stripe'});
|
|
@@ -291,7 +279,7 @@ describe('runConnect', () => {
|
|
|
291
279
|
});
|
|
292
280
|
|
|
293
281
|
it('should prompt for missing vars on reconnect without force', async () => {
|
|
294
|
-
|
|
282
|
+
mockStatSync.mockReturnValue({isDirectory: () => true});
|
|
295
283
|
mockReadPackageManifest.mockResolvedValue(bearerManifest);
|
|
296
284
|
mockFindMissingEnvVars.mockResolvedValue(['STRIPE_API_KEY']);
|
|
297
285
|
|
package/src/commands/connect.ts
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import {statSync} from 'node:fs';
|
|
7
8
|
import {join} from 'node:path';
|
|
8
9
|
|
|
9
10
|
import type {CommandModule} from 'yargs';
|
|
10
11
|
import prompts from 'prompts';
|
|
11
12
|
import {
|
|
12
|
-
|
|
13
|
-
addLockEntry,
|
|
14
|
-
buildLockFile,
|
|
15
|
-
discoverInstalledPackages,
|
|
16
|
-
ensureNpmContext,
|
|
13
|
+
ensurePackageJson,
|
|
17
14
|
findMissingEnvVars,
|
|
18
|
-
|
|
19
|
-
getNpmContextPaths,
|
|
20
|
-
npmInstall,
|
|
15
|
+
pmAdd,
|
|
21
16
|
readPackageManifest,
|
|
22
17
|
toNpmName,
|
|
23
18
|
upsertEnvEntries,
|
|
@@ -47,24 +42,23 @@ export async function runConnect(options: ConnectOptions): Promise<number> {
|
|
|
47
42
|
return 1;
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
const paths = await ensureNpmContext(repoPath);
|
|
51
45
|
const npmName = toNpmName(options.name);
|
|
52
|
-
const
|
|
53
|
-
|
|
46
|
+
const packageDir = join(repoPath, 'node_modules', npmName);
|
|
47
|
+
|
|
48
|
+
// Check if already installed by looking in node_modules
|
|
49
|
+
let isReconnect = false;
|
|
50
|
+
try {
|
|
51
|
+
isReconnect = statSync(packageDir).isDirectory();
|
|
52
|
+
} catch {
|
|
53
|
+
// Not installed
|
|
54
|
+
}
|
|
54
55
|
|
|
55
56
|
// Step 1: Install if fresh
|
|
56
57
|
if (!isReconnect) {
|
|
57
58
|
process.stderr.write(`[connect] Installing ${npmName}...\n`);
|
|
58
59
|
try {
|
|
59
|
-
|
|
60
|
-
await
|
|
61
|
-
version: result.version,
|
|
62
|
-
integrity: result.integrity,
|
|
63
|
-
});
|
|
64
|
-
await addConfigDep(repoPath, npmName, result.version);
|
|
65
|
-
// Rebuild lock file from what's actually installed
|
|
66
|
-
const discovered = await discoverInstalledPackages(paths);
|
|
67
|
-
await buildLockFile(repoPath, discovered);
|
|
60
|
+
ensurePackageJson(repoPath, 'amodal-project');
|
|
61
|
+
await pmAdd(repoPath, npmName);
|
|
68
62
|
} catch (err) {
|
|
69
63
|
const msg = err instanceof Error ? err.message : String(err);
|
|
70
64
|
process.stderr.write(`[connect] Install failed: ${msg}\n`);
|
|
@@ -75,9 +69,6 @@ export async function runConnect(options: ConnectOptions): Promise<number> {
|
|
|
75
69
|
}
|
|
76
70
|
|
|
77
71
|
// Step 2: Read manifest
|
|
78
|
-
const contextPaths = getNpmContextPaths(repoPath);
|
|
79
|
-
const packageDir = join(contextPaths.nodeModules, npmName);
|
|
80
|
-
|
|
81
72
|
let manifest;
|
|
82
73
|
try {
|
|
83
74
|
manifest = await readPackageManifest(packageDir);
|
|
@@ -230,3 +221,18 @@ export const connectCommand: CommandModule = {
|
|
|
230
221
|
process.exit(code);
|
|
231
222
|
},
|
|
232
223
|
};
|
|
224
|
+
|
|
225
|
+
/** Variant of connectCommand registered as `amodal connect connection <name>`. */
|
|
226
|
+
export const connectConnectionCommand: CommandModule = {
|
|
227
|
+
command: 'connection <name>',
|
|
228
|
+
describe: 'Connect a connection package (install + auth + test)',
|
|
229
|
+
builder: (yargs) =>
|
|
230
|
+
yargs
|
|
231
|
+
.positional('name', {type: 'string', demandOption: true, describe: 'Connection name'})
|
|
232
|
+
.option('force', {type: 'boolean', default: false, describe: 'Force re-authentication'}),
|
|
233
|
+
handler: async (argv) => {
|
|
234
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
235
|
+
const code = await runConnect({name: argv['name'] as string, force: argv['force'] as boolean});
|
|
236
|
+
process.exit(code);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {CommandModule} from 'yargs';
|
|
8
|
+
import {connectConnectionCommand} from '../connect.js';
|
|
9
|
+
import {connectChannelCommand} from '../connect-channel.js';
|
|
10
|
+
|
|
11
|
+
export const connectGroupCommand: CommandModule = {
|
|
12
|
+
command: 'connect <command>',
|
|
13
|
+
describe: 'Connect a package (connection or channel)',
|
|
14
|
+
builder: (yargs) =>
|
|
15
|
+
yargs
|
|
16
|
+
.command(connectConnectionCommand)
|
|
17
|
+
.command(connectChannelCommand)
|
|
18
|
+
.demandCommand(1, 'Specify: connection or channel'),
|
|
19
|
+
handler: () => {},
|
|
20
|
+
};
|
|
@@ -5,14 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {CommandModule} from 'yargs';
|
|
8
|
-
import {connectCommand} from '../connect.js';
|
|
9
8
|
import {installPkgCommand} from '../install-pkg.js';
|
|
10
9
|
import {uninstallCommand} from '../uninstall.js';
|
|
11
|
-
import {listCommand} from '../list.js';
|
|
12
|
-
import {updateCommand} from '../update.js';
|
|
13
|
-
import {diffCommand} from '../diff.js';
|
|
14
|
-
import {searchCommand} from '../search.js';
|
|
15
|
-
import {publishCommand} from '../publish.js';
|
|
16
10
|
import {linkCommand} from '../link.js';
|
|
17
11
|
import {syncCommand} from '../sync.js';
|
|
18
12
|
|
|
@@ -21,14 +15,8 @@ export const pkgCommand: CommandModule = {
|
|
|
21
15
|
describe: 'Manage packages',
|
|
22
16
|
builder: (yargs) =>
|
|
23
17
|
yargs
|
|
24
|
-
.command(connectCommand)
|
|
25
18
|
.command(installPkgCommand)
|
|
26
19
|
.command(uninstallCommand)
|
|
27
|
-
.command(listCommand)
|
|
28
|
-
.command(updateCommand)
|
|
29
|
-
.command(diffCommand)
|
|
30
|
-
.command(searchCommand)
|
|
31
|
-
.command(publishCommand)
|
|
32
20
|
.command(linkCommand)
|
|
33
21
|
.command(syncCommand)
|
|
34
22
|
.demandCommand(1, 'Specify a subcommand'),
|
package/src/commands/index.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
|
|
7
9
|
import {initCommand} from './init.js';
|
|
8
10
|
import {devCommand} from './dev.js';
|
|
9
11
|
import {inspectCommand} from './inspect.js';
|
|
@@ -15,12 +17,13 @@ import {pkgCommand} from './groups/pkg.js';
|
|
|
15
17
|
import {deployGroupCommand} from './groups/deploy.js';
|
|
16
18
|
import {opsCommand} from './groups/ops.js';
|
|
17
19
|
import {authCommand} from './groups/auth.js';
|
|
20
|
+
import {connectGroupCommand} from './groups/connect.js';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* All amodal subcommands registered on the root yargs instance.
|
|
21
24
|
*
|
|
22
25
|
* Top-level: daily-driver commands (init, dev, chat, validate, inspect, eval, test)
|
|
23
|
-
* Groups: pkg, deploy, ops, auth
|
|
26
|
+
* Groups: pkg, deploy, ops, auth, connect
|
|
24
27
|
*/
|
|
25
28
|
export const amodalCommands = [
|
|
26
29
|
// Top-level
|
|
@@ -36,4 +39,5 @@ export const amodalCommands = [
|
|
|
36
39
|
deployGroupCommand,
|
|
37
40
|
opsCommand,
|
|
38
41
|
authCommand,
|
|
42
|
+
connectGroupCommand,
|
|
39
43
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -36,7 +36,7 @@ describe('runInit', () => {
|
|
|
36
36
|
expect(existsSync(join(tempDir, 'evals'))).toBe(true);
|
|
37
37
|
expect(existsSync(join(tempDir, '.gitignore'))).toBe(true);
|
|
38
38
|
const gitignore = readFileSync(join(tempDir, '.gitignore'), 'utf-8');
|
|
39
|
-
expect(gitignore).toContain('
|
|
39
|
+
expect(gitignore).toContain('node_modules/');
|
|
40
40
|
expect(gitignore).toContain('.env');
|
|
41
41
|
});
|
|
42
42
|
|
package/src/commands/init.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import {existsSync, mkdirSync, writeFileSync} from 'node:fs';
|
|
8
8
|
import {join} from 'node:path';
|
|
9
9
|
import type {CommandModule} from 'yargs';
|
|
10
|
+
import {ensurePackageJson} from '@amodalai/core';
|
|
10
11
|
import {generateConfigTemplate} from '../templates/config-template.js';
|
|
11
12
|
|
|
12
13
|
export interface InitOptions {
|
|
@@ -46,10 +47,13 @@ export async function runInit(options: InitOptions = {}): Promise<void> {
|
|
|
46
47
|
// Write amodal.json at repo root
|
|
47
48
|
writeFileSync(configPath, generateConfigTemplate({name, provider}));
|
|
48
49
|
|
|
50
|
+
// Ensure package.json exists
|
|
51
|
+
ensurePackageJson(cwd, name);
|
|
52
|
+
|
|
49
53
|
// Write .gitignore if it doesn't exist
|
|
50
54
|
const gitignorePath = join(cwd, '.gitignore');
|
|
51
55
|
if (!existsSync(gitignorePath)) {
|
|
52
|
-
writeFileSync(gitignorePath, '.amodal/\
|
|
56
|
+
writeFileSync(gitignorePath, '.amodal/\nnode_modules/\n.env\n.env.*\n');
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
process.stderr.write(`[init] Created project "${name}" (${provider})\n`);
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license
|
|
3
|
-
* Copyright
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
8
8
|
|
|
9
9
|
const mockFindRepoRoot = vi.fn(() => '/test/repo');
|
|
10
|
-
const mockReadLockFile = vi.fn();
|
|
11
10
|
const mockResolveAllPackages = vi.fn();
|
|
12
11
|
|
|
13
12
|
vi.mock('../shared/repo-discovery.js', () => ({
|
|
@@ -15,7 +14,6 @@ vi.mock('../shared/repo-discovery.js', () => ({
|
|
|
15
14
|
}));
|
|
16
15
|
|
|
17
16
|
vi.mock('@amodalai/core', () => ({
|
|
18
|
-
readLockFile: mockReadLockFile,
|
|
19
17
|
resolveAllPackages: mockResolveAllPackages,
|
|
20
18
|
loadRepo: vi.fn().mockResolvedValue({
|
|
21
19
|
config: {models: {main: {provider: 'anthropic', model: 'test'}}},
|
|
@@ -130,7 +128,6 @@ describe('runInspect', () => {
|
|
|
130
128
|
|
|
131
129
|
// Resolved package tests
|
|
132
130
|
it('should show resolved packages when resolved flag set', async () => {
|
|
133
|
-
mockReadLockFile.mockResolvedValue({lockVersion: 2, packages: {}});
|
|
134
131
|
mockResolveAllPackages.mockResolvedValue({
|
|
135
132
|
connections: new Map([['salesforce', {surface: [{}, {}, {}], spec: {auth: {type: 'bearer'}}}]]),
|
|
136
133
|
skills: [{name: 'triage', body: 'Triage methodology content'}],
|
|
@@ -149,7 +146,6 @@ describe('runInspect', () => {
|
|
|
149
146
|
});
|
|
150
147
|
|
|
151
148
|
it('should filter by scope', async () => {
|
|
152
|
-
mockReadLockFile.mockResolvedValue({lockVersion: 2, packages: {}});
|
|
153
149
|
mockResolveAllPackages.mockResolvedValue({
|
|
154
150
|
connections: new Map([
|
|
155
151
|
['salesforce', {surface: [{}, {}], spec: {auth: {type: 'bearer'}}}],
|
|
@@ -169,18 +165,7 @@ describe('runInspect', () => {
|
|
|
169
165
|
expect(output).not.toContain('stripe');
|
|
170
166
|
});
|
|
171
167
|
|
|
172
|
-
it('should show no lock file message when resolved without lock', async () => {
|
|
173
|
-
mockReadLockFile.mockResolvedValue(null);
|
|
174
|
-
|
|
175
|
-
const {runInspect} = await import('./inspect.js');
|
|
176
|
-
await runInspect({resolved: true});
|
|
177
|
-
|
|
178
|
-
const output = stdoutOutput.join('');
|
|
179
|
-
expect(output).toContain('No lock file found');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
168
|
it('should show resolution warnings', async () => {
|
|
183
|
-
mockReadLockFile.mockResolvedValue({lockVersion: 2, packages: {}});
|
|
184
169
|
mockResolveAllPackages.mockResolvedValue({
|
|
185
170
|
connections: new Map(),
|
|
186
171
|
skills: [],
|
|
@@ -198,7 +183,6 @@ describe('runInspect', () => {
|
|
|
198
183
|
});
|
|
199
184
|
|
|
200
185
|
it('should show connection details in resolved view', async () => {
|
|
201
|
-
mockReadLockFile.mockResolvedValue({lockVersion: 2, packages: {}});
|
|
202
186
|
mockResolveAllPackages.mockResolvedValue({
|
|
203
187
|
connections: new Map([['crm', {surface: [{}, {}], spec: {auth: {type: 'oauth2'}}}]]),
|
|
204
188
|
skills: [],
|
|
@@ -216,7 +200,6 @@ describe('runInspect', () => {
|
|
|
216
200
|
});
|
|
217
201
|
|
|
218
202
|
it('should show skill body length in resolved view', async () => {
|
|
219
|
-
mockReadLockFile.mockResolvedValue({lockVersion: 2, packages: {}});
|
|
220
203
|
mockResolveAllPackages.mockResolvedValue({
|
|
221
204
|
connections: new Map(),
|
|
222
205
|
skills: [{name: 'investigate', body: 'A'.repeat(500)}],
|