@forgeportal/cli 1.3.0

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.
@@ -0,0 +1,4 @@
1
+
2
+ > @forgeportal/cli@1.3.0 build /home/runner/work/forgeportal/forgeportal/packages/cli
3
+ > tsc
4
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bendaamerahmed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sync.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sync.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,185 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { syncCommand } from '../commands/sync.js';
6
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
7
+ function makeTmpRepo(opts) {
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-sync-test-'));
9
+ // forgeportal.yaml
10
+ const pluginList = opts.configPlugins?.map((p) => ` - "${p}"`).join('\n') ?? '';
11
+ fs.writeFileSync(path.join(root, 'forgeportal.yaml'), `
12
+ pluginPackages:
13
+ packages:
14
+ ${pluginList}
15
+ `.trimStart());
16
+ // apps/api/package.json
17
+ const apiDir = path.join(root, 'apps', 'api');
18
+ fs.mkdirSync(apiDir, { recursive: true });
19
+ fs.writeFileSync(path.join(apiDir, 'package.json'), JSON.stringify({
20
+ name: '@forgeportal/api',
21
+ dependencies: opts.apiDeps ?? {},
22
+ }, null, 2));
23
+ // apps/ui/package.json
24
+ const uiDir = path.join(root, 'apps', 'ui');
25
+ fs.mkdirSync(uiDir, { recursive: true });
26
+ fs.writeFileSync(path.join(uiDir, 'package.json'), JSON.stringify({
27
+ name: '@forgeportal/ui',
28
+ dependencies: opts.uiDeps ?? {},
29
+ }, null, 2));
30
+ // workspace plugins (simulate packages/ directory)
31
+ for (const plugin of opts.workspacePlugins ?? []) {
32
+ const pluginDir = path.join(root, 'packages', plugin.name.replace('@forgeportal/', ''));
33
+ fs.mkdirSync(pluginDir, { recursive: true });
34
+ fs.writeFileSync(path.join(pluginDir, 'package.json'), JSON.stringify({
35
+ name: plugin.name,
36
+ version: plugin.version ?? '1.0.0',
37
+ }, null, 2));
38
+ fs.writeFileSync(path.join(pluginDir, 'forgeportal-plugin.json'), JSON.stringify({
39
+ version: plugin.version ?? '1.0.0',
40
+ forgeportal: { type: plugin.type },
41
+ }, null, 2));
42
+ }
43
+ return root;
44
+ }
45
+ function readDeps(root, app) {
46
+ const pkgPath = path.join(root, 'apps', app, 'package.json');
47
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
48
+ return pkg.dependencies ?? {};
49
+ }
50
+ // ─── Mock execSync so pnpm install doesn't run in tests ──────────────────────
51
+ vi.mock('node:child_process', async (importOriginal) => {
52
+ const original = await importOriginal();
53
+ return {
54
+ ...original,
55
+ execSync: vi.fn((cmd, _opts) => {
56
+ if (String(cmd).startsWith('pnpm install'))
57
+ return '';
58
+ // Pass npm view to the original (we mock it per-test when needed)
59
+ throw new Error(`Unexpected execSync: ${cmd}`);
60
+ }),
61
+ };
62
+ });
63
+ // ─── Tests ────────────────────────────────────────────────────────────────────
64
+ describe('forge sync — workspace plugin', () => {
65
+ it('adds a fullstack plugin to both api and ui', async () => {
66
+ const root = makeTmpRepo({
67
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
68
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
69
+ });
70
+ const result = await syncCommand({ root, ci: true });
71
+ expect(result.changed).toBe(true);
72
+ expect(result.apiAdded).toContain('@forgeportal/plugin-kubernetes');
73
+ expect(result.uiAdded).toContain('@forgeportal/plugin-kubernetes');
74
+ expect(readDeps(root, 'api')['@forgeportal/plugin-kubernetes']).toBe('workspace:*');
75
+ expect(readDeps(root, 'ui')['@forgeportal/plugin-kubernetes']).toBe('workspace:*');
76
+ });
77
+ it('adds a backend-only plugin only to api', async () => {
78
+ const root = makeTmpRepo({
79
+ configPlugins: ['@forgeportal/plugin-backend-only'],
80
+ workspacePlugins: [{ name: '@forgeportal/plugin-backend-only', type: 'backend' }],
81
+ });
82
+ await syncCommand({ root, ci: true });
83
+ expect(readDeps(root, 'api')['@forgeportal/plugin-backend-only']).toBe('workspace:*');
84
+ expect(readDeps(root, 'ui')['@forgeportal/plugin-backend-only']).toBeUndefined();
85
+ });
86
+ it('adds a ui-only plugin only to ui', async () => {
87
+ const root = makeTmpRepo({
88
+ configPlugins: ['@forgeportal/plugin-ui-only'],
89
+ workspacePlugins: [{ name: '@forgeportal/plugin-ui-only', type: 'ui' }],
90
+ });
91
+ await syncCommand({ root, ci: true });
92
+ expect(readDeps(root, 'api')['@forgeportal/plugin-ui-only']).toBeUndefined();
93
+ expect(readDeps(root, 'ui')['@forgeportal/plugin-ui-only']).toBe('workspace:*');
94
+ });
95
+ it('removes a plugin no longer in forgeportal.yaml', async () => {
96
+ const root = makeTmpRepo({
97
+ configPlugins: [],
98
+ apiDeps: { '@forgeportal/plugin-old': '^1.0.0' },
99
+ uiDeps: { '@forgeportal/plugin-old': '^1.0.0' },
100
+ });
101
+ // forgeportal.yaml has no plugins now
102
+ fs.writeFileSync(path.join(root, 'forgeportal.yaml'), 'pluginPackages:\n packages: []\n');
103
+ await syncCommand({ root, ci: true });
104
+ expect(readDeps(root, 'api')['@forgeportal/plugin-old']).toBeUndefined();
105
+ expect(readDeps(root, 'ui')['@forgeportal/plugin-old']).toBeUndefined();
106
+ });
107
+ it('does not touch non-plugin deps when removing', async () => {
108
+ const root = makeTmpRepo({
109
+ configPlugins: [],
110
+ apiDeps: {
111
+ 'express': '^4.18.0',
112
+ '@forgeportal/plugin-old': '^1.0.0',
113
+ '@forgeportal/core': '^1.0.0',
114
+ },
115
+ });
116
+ fs.writeFileSync(path.join(root, 'forgeportal.yaml'), 'pluginPackages:\n packages: []\n');
117
+ await syncCommand({ root, ci: true });
118
+ const deps = readDeps(root, 'api');
119
+ expect(deps['express']).toBe('^4.18.0');
120
+ expect(deps['@forgeportal/core']).toBe('^1.0.0');
121
+ expect(deps['@forgeportal/plugin-old']).toBeUndefined();
122
+ });
123
+ it('returns changed:false when already in sync', async () => {
124
+ const root = makeTmpRepo({
125
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
126
+ apiDeps: { '@forgeportal/plugin-kubernetes': 'workspace:*' },
127
+ uiDeps: { '@forgeportal/plugin-kubernetes': 'workspace:*' },
128
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
129
+ });
130
+ const result = await syncCommand({ root, ci: true });
131
+ expect(result.changed).toBe(false);
132
+ });
133
+ });
134
+ describe('forge sync --dry-run', () => {
135
+ it('does not modify package.json files', async () => {
136
+ const root = makeTmpRepo({
137
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
138
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
139
+ });
140
+ const before = JSON.stringify(readDeps(root, 'api'));
141
+ await syncCommand({ root, dryRun: true });
142
+ expect(JSON.stringify(readDeps(root, 'api'))).toBe(before);
143
+ });
144
+ it('reports changed:true without writing', async () => {
145
+ const root = makeTmpRepo({
146
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
147
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
148
+ });
149
+ const result = await syncCommand({ root, dryRun: true });
150
+ expect(result.changed).toBe(true);
151
+ expect(readDeps(root, 'api')['@forgeportal/plugin-kubernetes']).toBeUndefined();
152
+ });
153
+ });
154
+ describe('forge sync --check', () => {
155
+ it('exits 1 when out of sync', async () => {
156
+ const root = makeTmpRepo({
157
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
158
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
159
+ });
160
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((_code) => {
161
+ throw new Error(`process.exit(${_code})`);
162
+ });
163
+ await expect(syncCommand({ root, check: true })).rejects.toThrow('process.exit(1)');
164
+ expect(exitSpy).toHaveBeenCalledWith(1);
165
+ exitSpy.mockRestore();
166
+ });
167
+ it('exits 0 when in sync', async () => {
168
+ const root = makeTmpRepo({
169
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
170
+ apiDeps: { '@forgeportal/plugin-kubernetes': 'workspace:*' },
171
+ uiDeps: { '@forgeportal/plugin-kubernetes': 'workspace:*' },
172
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
173
+ });
174
+ const result = await syncCommand({ root, check: true });
175
+ expect(result.changed).toBe(false);
176
+ });
177
+ });
178
+ describe('forge sync — no forgeportal.yaml', () => {
179
+ it('returns empty result gracefully when no yaml exists', async () => {
180
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-sync-noconfig-'));
181
+ const result = await syncCommand({ root, ci: true });
182
+ expect(result.changed).toBe(false);
183
+ });
184
+ });
185
+ //# sourceMappingURL=sync.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.test.js","sourceRoot":"","sources":["../../src/__tests__/sync.test.ts"],"names":[],"mappings":"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"}
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * forge — ForgePortal CLI
4
+ *
5
+ * Usage:
6
+ * forge sync # sync plugin deps + pnpm install
7
+ * forge sync --dry-run # show diff, no writes
8
+ * forge sync --ci # write only, no pnpm install (Dockerfile use)
9
+ * forge sync --check # exit 1 if out of sync (CI gate)
10
+ */
11
+ import { syncCommand } from './commands/sync.js';
12
+ const args = process.argv.slice(2);
13
+ const command = args[0];
14
+ if (!command || command === '--help' || command === '-h') {
15
+ console.log(`
16
+ forge — ForgePortal CLI
17
+
18
+ Commands:
19
+ sync Sync plugin dependencies from forgeportal.yaml → package.json files
20
+ sync --dry-run Show changes without writing any files
21
+ sync --check Exit 1 if package.json files are out of sync (CI gate)
22
+ sync --ci Write files without running pnpm install (for Dockerfile)
23
+ `);
24
+ process.exit(0);
25
+ }
26
+ if (command === 'sync') {
27
+ const dryRun = args.includes('--dry-run');
28
+ const ci = args.includes('--ci');
29
+ const check = args.includes('--check');
30
+ const configFlag = args.indexOf('--config');
31
+ const configPath = configFlag !== -1 ? args[configFlag + 1] : undefined;
32
+ syncCommand({ dryRun, ci, check, configPath }).catch((err) => {
33
+ console.error('forge sync failed:', err instanceof Error ? err.message : String(err));
34
+ process.exit(1);
35
+ });
36
+ }
37
+ else {
38
+ console.error(`Unknown command: "${command}". Run "forge --help" for usage.`);
39
+ process.exit(1);
40
+ }
41
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,IAAI,GAAM,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACtC,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAExB,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;IACzD,OAAO,CAAC,GAAG,CAAC;;;;;;;;CAQb,CAAC,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;IACvB,MAAM,MAAM,GAAO,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC9C,MAAM,EAAE,GAAW,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,KAAK,GAAQ,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAExE,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACpE,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACtF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;KAAM,CAAC;IACN,OAAO,CAAC,KAAK,CAAC,qBAAqB,OAAO,kCAAkC,CAAC,CAAC;IAC9E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC"}
@@ -0,0 +1,27 @@
1
+ export interface SyncOptions {
2
+ dryRun?: boolean;
3
+ ci?: boolean;
4
+ check?: boolean;
5
+ configPath?: string;
6
+ /** Override the monorepo root (useful for tests). Defaults to process.cwd(). */
7
+ root?: string;
8
+ }
9
+ export interface SyncResult {
10
+ apiAdded: string[];
11
+ apiRemoved: string[];
12
+ uiAdded: string[];
13
+ uiRemoved: string[];
14
+ changed: boolean;
15
+ }
16
+ /**
17
+ * `forge sync` — synchronises plugin dependencies from forgeportal.yaml
18
+ * into apps/api/package.json and apps/ui/package.json.
19
+ *
20
+ * Modes:
21
+ * default write + pnpm install
22
+ * --dry-run print diff, no writes
23
+ * --check exit 1 if out of sync (CI gate)
24
+ * --ci write files, skip pnpm install (Dockerfile use)
25
+ */
26
+ export declare function syncCommand(opts?: SyncOptions): Promise<SyncResult>;
27
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAK,OAAO,CAAC;IACpB,EAAE,CAAC,EAAS,OAAO,CAAC;IACpB,KAAK,CAAC,EAAM,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,IAAI,CAAC,EAAO,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAI,MAAM,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAK,MAAM,EAAE,CAAC;IACrB,SAAS,EAAG,MAAM,EAAE,CAAC;IACrB,OAAO,EAAK,OAAO,CAAC;CACrB;AAmBD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CA2G7E"}
@@ -0,0 +1,229 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
5
+ import yaml from 'js-yaml';
6
+ // ─── Main command ─────────────────────────────────────────────────────────────
7
+ /**
8
+ * `forge sync` — synchronises plugin dependencies from forgeportal.yaml
9
+ * into apps/api/package.json and apps/ui/package.json.
10
+ *
11
+ * Modes:
12
+ * default write + pnpm install
13
+ * --dry-run print diff, no writes
14
+ * --check exit 1 if out of sync (CI gate)
15
+ * --ci write files, skip pnpm install (Dockerfile use)
16
+ */
17
+ export async function syncCommand(opts = {}) {
18
+ const root = opts.root ?? process.cwd();
19
+ const cfgPath = opts.configPath ?? path.join(root, 'forgeportal.yaml');
20
+ // ── 1. Read forgeportal.yaml ──────────────────────────────────────────────
21
+ if (!fs.existsSync(cfgPath)) {
22
+ if (opts.ci) {
23
+ console.log('[forge sync] No forgeportal.yaml found — nothing to sync.');
24
+ return emptyResult();
25
+ }
26
+ console.warn(`[forge sync] Warning: ${cfgPath} not found. Nothing to sync.`);
27
+ return emptyResult();
28
+ }
29
+ const config = yaml.load(fs.readFileSync(cfgPath, 'utf8'));
30
+ const packages = config.pluginPackages?.packages ?? [];
31
+ // ── 2. Resolve manifests ──────────────────────────────────────────────────
32
+ if (packages.length === 0) {
33
+ console.log('[forge sync] No plugins configured — will remove any installed forge plugins.');
34
+ }
35
+ const resolved = [];
36
+ for (const pkg of packages) {
37
+ try {
38
+ const info = await resolveManifest(pkg, root);
39
+ resolved.push(info);
40
+ }
41
+ catch (err) {
42
+ const msg = err instanceof Error ? err.message : String(err);
43
+ if (opts.ci) {
44
+ console.error(`[forge sync] ERROR: Cannot resolve manifest for "${pkg}": ${msg}`);
45
+ process.exit(1);
46
+ }
47
+ console.warn(`[forge sync] Warning: Cannot resolve manifest for "${pkg}": ${msg}. Skipping.`);
48
+ }
49
+ }
50
+ // ── 3. Build desired dep maps ─────────────────────────────────────────────
51
+ const apiDeps = {};
52
+ const uiDeps = {};
53
+ for (const { packageName, version, pluginType } of resolved) {
54
+ const versionSpec = version.startsWith('workspace:') ? version : `^${version}`;
55
+ if (pluginType === 'backend' || pluginType === 'fullstack')
56
+ apiDeps[packageName] = versionSpec;
57
+ if (pluginType === 'ui' || pluginType === 'fullstack')
58
+ uiDeps[packageName] = versionSpec;
59
+ }
60
+ // ── 4. Apply to package.json files ────────────────────────────────────────
61
+ const apiPkgPath = path.join(root, 'apps', 'api', 'package.json');
62
+ const uiPkgPath = path.join(root, 'apps', 'ui', 'package.json');
63
+ const apiDiff = computeDiff(apiPkgPath, apiDeps);
64
+ const uiDiff = computeDiff(uiPkgPath, uiDeps);
65
+ const changed = apiDiff.changed || uiDiff.changed;
66
+ // ── 5. Handle modes ───────────────────────────────────────────────────────
67
+ if (opts.check) {
68
+ if (changed) {
69
+ console.error('✗ package.json files are out of sync with forgeportal.yaml.\n' +
70
+ ' Run "pnpm forge:sync" to fix.\n');
71
+ printDiff(apiPkgPath, apiDiff);
72
+ printDiff(uiPkgPath, uiDiff);
73
+ process.exit(1);
74
+ }
75
+ console.log('✓ package.json files are in sync with forgeportal.yaml.');
76
+ return toResult(apiDiff, uiDiff);
77
+ }
78
+ if (opts.dryRun) {
79
+ console.log('[forge sync --dry-run] Changes that would be applied:\n');
80
+ printDiff(apiPkgPath, apiDiff);
81
+ printDiff(uiPkgPath, uiDiff);
82
+ if (!changed)
83
+ console.log(' (no changes needed)');
84
+ return toResult(apiDiff, uiDiff);
85
+ }
86
+ if (!changed) {
87
+ console.log('✓ package.json files already in sync — nothing to do.');
88
+ return emptyResult();
89
+ }
90
+ // Write files
91
+ applyDiff(apiPkgPath, apiDiff);
92
+ applyDiff(uiPkgPath, uiDiff);
93
+ const totalAdded = apiDiff.toAdd.length + uiDiff.toAdd.length;
94
+ const totalRemoved = apiDiff.toRemove.length + uiDiff.toRemove.length;
95
+ console.log(`✓ Synced: +${totalAdded} dep(s) added, -${totalRemoved} dep(s) removed across api/ui.`);
96
+ // ── 6. pnpm install (skip in --ci) ────────────────────────────────────────
97
+ if (!opts.ci) {
98
+ console.log(' Running pnpm install…');
99
+ execSync('pnpm install', { stdio: 'inherit', cwd: root });
100
+ console.log('✓ pnpm install complete.');
101
+ }
102
+ return toResult(apiDiff, uiDiff);
103
+ }
104
+ // ─── Manifest resolution ──────────────────────────────────────────────────────
105
+ /**
106
+ * Resolves a plugin manifest from the workspace, node_modules, or the npm registry.
107
+ * Workspace packages always take priority and receive the "workspace:*" version specifier.
108
+ */
109
+ async function resolveManifest(packageName, root) {
110
+ // ── 1. Workspace scan: packages directory ─────────────────────────────────
111
+ const pkgsDir = path.join(root, 'packages');
112
+ if (fs.existsSync(pkgsDir)) {
113
+ for (const dir of fs.readdirSync(pkgsDir)) {
114
+ const pkgJsonPath = path.join(pkgsDir, dir, 'package.json');
115
+ const manifestPath = path.join(pkgsDir, dir, 'forgeportal-plugin.json');
116
+ if (!fs.existsSync(pkgJsonPath) || !fs.existsSync(manifestPath))
117
+ continue;
118
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
119
+ if (pkg.name !== packageName)
120
+ continue;
121
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
122
+ return {
123
+ packageName,
124
+ version: 'workspace:*',
125
+ pluginType: manifest.forgeportal.type,
126
+ };
127
+ }
128
+ }
129
+ // ── 2. node_modules via require.resolve (external installed package) ───────
130
+ try {
131
+ const req = createRequire(path.join(root, '_placeholder_.js'));
132
+ const pkgDir = path.dirname(req.resolve(`${packageName}/package.json`));
133
+ const manifest = JSON.parse(fs.readFileSync(path.join(pkgDir, 'forgeportal-plugin.json'), 'utf8'));
134
+ // If the resolved package lives under any "packages/" directory (workspace
135
+ // symlink or monorepo root), always use "workspace:*" so pnpm is happy.
136
+ const normalizedPkgDir = pkgDir.replace(/\\/g, '/');
137
+ const isWorkspaceLink = [root, process.cwd()].some((base) => normalizedPkgDir.startsWith(path.join(base, 'packages').replace(/\\/g, '/')));
138
+ return {
139
+ packageName,
140
+ version: isWorkspaceLink ? 'workspace:*' : manifest.version,
141
+ pluginType: manifest.forgeportal.type,
142
+ };
143
+ }
144
+ catch {
145
+ // not installed in node_modules — try npm registry
146
+ }
147
+ // ── 3. Fall back to npm registry ──────────────────────────────────────────
148
+ try {
149
+ const raw = execSync(`npm view ${packageName} version --json 2>/dev/null`, { encoding: 'utf8', timeout: 15_000 }).trim();
150
+ const version = JSON.parse(raw);
151
+ // Fetch forgeportal field to determine type
152
+ const meta = execSync(`npm view ${packageName} forgeportal --json 2>/dev/null`, { encoding: 'utf8', timeout: 15_000 }).trim();
153
+ const forgeportal = JSON.parse(meta);
154
+ const pluginType = (forgeportal?.type ?? 'backend');
155
+ return { packageName, version, pluginType };
156
+ }
157
+ catch {
158
+ throw new Error(`Plugin "${packageName}" not found in workspace packages, node_modules, or npm registry.`);
159
+ }
160
+ }
161
+ /**
162
+ * Computes the diff between current deps in package.json and desired deps.
163
+ * Also removes plugin deps that are no longer configured.
164
+ */
165
+ function computeDiff(pkgPath, desiredDeps) {
166
+ if (!fs.existsSync(pkgPath)) {
167
+ return { changed: false, toAdd: [], toRemove: [] };
168
+ }
169
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
170
+ const current = pkg.dependencies ?? {};
171
+ const toRemove = Object.keys(current).filter((dep) => isPluginPackage(dep) && !(dep in desiredDeps));
172
+ const toAdd = Object.entries(desiredDeps).filter(([dep, ver]) => current[dep] !== ver);
173
+ return {
174
+ changed: toRemove.length > 0 || toAdd.length > 0,
175
+ toAdd,
176
+ toRemove,
177
+ };
178
+ }
179
+ function applyDiff(pkgPath, diff) {
180
+ if (!diff.changed || !fs.existsSync(pkgPath))
181
+ return;
182
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
183
+ const current = pkg.dependencies ?? {};
184
+ for (const dep of diff.toRemove)
185
+ delete current[dep];
186
+ for (const [dep, ver] of diff.toAdd)
187
+ current[dep] = ver;
188
+ pkg.dependencies = sortKeys(current);
189
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
190
+ const label = path.relative(process.cwd(), pkgPath);
191
+ console.log(` ✓ ${label}: +${diff.toAdd.length} -${diff.toRemove.length}`);
192
+ }
193
+ function printDiff(pkgPath, diff) {
194
+ if (!diff.changed)
195
+ return;
196
+ const label = path.relative(process.cwd(), pkgPath);
197
+ console.log(` ${label}:`);
198
+ for (const [dep, ver] of diff.toAdd)
199
+ console.log(` + ${dep}@${ver}`);
200
+ for (const dep of diff.toRemove)
201
+ console.log(` - ${dep}`);
202
+ }
203
+ function sortKeys(obj) {
204
+ return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
205
+ }
206
+ /**
207
+ * Returns true if the package is a user-installable ForgePortal plugin
208
+ * managed by `forge sync`. Excludes framework packages like @forgeportal/plugin-sdk.
209
+ */
210
+ function isPluginPackage(name) {
211
+ // plugin-sdk is a framework/SDK package — never remove it automatically.
212
+ if (name === '@forgeportal/plugin-sdk')
213
+ return false;
214
+ return name.startsWith('@forgeportal/plugin-') || /^forge-plugin-/.test(name) || /^@[^/]+\/forge-plugin-/.test(name);
215
+ }
216
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
217
+ function emptyResult() {
218
+ return { apiAdded: [], apiRemoved: [], uiAdded: [], uiRemoved: [], changed: false };
219
+ }
220
+ function toResult(apiDiff, uiDiff) {
221
+ return {
222
+ apiAdded: apiDiff.toAdd.map(([d]) => d),
223
+ apiRemoved: apiDiff.toRemove,
224
+ uiAdded: uiDiff.toAdd.map(([d]) => d),
225
+ uiRemoved: uiDiff.toRemove,
226
+ changed: apiDiff.changed || uiDiff.changed,
227
+ };
228
+ }
229
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { syncCommand } from './commands/sync.js';
2
+ export type { SyncOptions, SyncResult } from './commands/sync.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { syncCommand } from './commands/sync.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@forgeportal/cli",
3
+ "version": "1.3.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "forge": "./dist/bin.js"
7
+ },
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "main": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dependencies": {
20
+ "js-yaml": "^4.1.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/js-yaml": "^4.0.9",
24
+ "@types/node": "*",
25
+ "vitest": "*"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "test": "vitest run",
30
+ "lint": "eslint src/",
31
+ "clean": "rm -rf dist"
32
+ }
33
+ }
@@ -0,0 +1,226 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { syncCommand } from '../commands/sync.js';
6
+
7
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
8
+
9
+ function makeTmpRepo(opts: {
10
+ configPlugins?: string[];
11
+ apiDeps?: Record<string, string>;
12
+ uiDeps?: Record<string, string>;
13
+ workspacePlugins?: Array<{ name: string; type: 'ui' | 'backend' | 'fullstack'; version?: string }>;
14
+ }): string {
15
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-sync-test-'));
16
+
17
+ // forgeportal.yaml
18
+ const pluginList = opts.configPlugins?.map((p) => ` - "${p}"`).join('\n') ?? '';
19
+ fs.writeFileSync(path.join(root, 'forgeportal.yaml'), `
20
+ pluginPackages:
21
+ packages:
22
+ ${pluginList}
23
+ `.trimStart());
24
+
25
+ // apps/api/package.json
26
+ const apiDir = path.join(root, 'apps', 'api');
27
+ fs.mkdirSync(apiDir, { recursive: true });
28
+ fs.writeFileSync(path.join(apiDir, 'package.json'), JSON.stringify({
29
+ name: '@forgeportal/api',
30
+ dependencies: opts.apiDeps ?? {},
31
+ }, null, 2));
32
+
33
+ // apps/ui/package.json
34
+ const uiDir = path.join(root, 'apps', 'ui');
35
+ fs.mkdirSync(uiDir, { recursive: true });
36
+ fs.writeFileSync(path.join(uiDir, 'package.json'), JSON.stringify({
37
+ name: '@forgeportal/ui',
38
+ dependencies: opts.uiDeps ?? {},
39
+ }, null, 2));
40
+
41
+ // workspace plugins (simulate packages/ directory)
42
+ for (const plugin of opts.workspacePlugins ?? []) {
43
+ const pluginDir = path.join(root, 'packages', plugin.name.replace('@forgeportal/', ''));
44
+ fs.mkdirSync(pluginDir, { recursive: true });
45
+ fs.writeFileSync(path.join(pluginDir, 'package.json'), JSON.stringify({
46
+ name: plugin.name,
47
+ version: plugin.version ?? '1.0.0',
48
+ }, null, 2));
49
+ fs.writeFileSync(path.join(pluginDir, 'forgeportal-plugin.json'), JSON.stringify({
50
+ version: plugin.version ?? '1.0.0',
51
+ forgeportal: { type: plugin.type },
52
+ }, null, 2));
53
+ }
54
+
55
+ return root;
56
+ }
57
+
58
+ function readDeps(root: string, app: 'api' | 'ui'): Record<string, string> {
59
+ const pkgPath = path.join(root, 'apps', app, 'package.json');
60
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { dependencies?: Record<string, string> };
61
+ return pkg.dependencies ?? {};
62
+ }
63
+
64
+ // ─── Mock execSync so pnpm install doesn't run in tests ──────────────────────
65
+
66
+ vi.mock('node:child_process', async (importOriginal) => {
67
+ const original = await importOriginal<typeof import('node:child_process')>();
68
+ return {
69
+ ...original,
70
+ execSync: vi.fn((cmd: string, _opts?: unknown) => {
71
+ if (String(cmd).startsWith('pnpm install')) return '';
72
+ // Pass npm view to the original (we mock it per-test when needed)
73
+ throw new Error(`Unexpected execSync: ${cmd}`);
74
+ }),
75
+ };
76
+ });
77
+
78
+ // ─── Tests ────────────────────────────────────────────────────────────────────
79
+
80
+ describe('forge sync — workspace plugin', () => {
81
+ it('adds a fullstack plugin to both api and ui', async () => {
82
+ const root = makeTmpRepo({
83
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
84
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
85
+ });
86
+
87
+ const result = await syncCommand({ root, ci: true });
88
+
89
+ expect(result.changed).toBe(true);
90
+ expect(result.apiAdded).toContain('@forgeportal/plugin-kubernetes');
91
+ expect(result.uiAdded).toContain('@forgeportal/plugin-kubernetes');
92
+ expect(readDeps(root, 'api')['@forgeportal/plugin-kubernetes']).toBe('workspace:*');
93
+ expect(readDeps(root, 'ui')['@forgeportal/plugin-kubernetes']).toBe('workspace:*');
94
+ });
95
+
96
+ it('adds a backend-only plugin only to api', async () => {
97
+ const root = makeTmpRepo({
98
+ configPlugins: ['@forgeportal/plugin-backend-only'],
99
+ workspacePlugins: [{ name: '@forgeportal/plugin-backend-only', type: 'backend' }],
100
+ });
101
+
102
+ await syncCommand({ root, ci: true });
103
+
104
+ expect(readDeps(root, 'api')['@forgeportal/plugin-backend-only']).toBe('workspace:*');
105
+ expect(readDeps(root, 'ui')['@forgeportal/plugin-backend-only']).toBeUndefined();
106
+ });
107
+
108
+ it('adds a ui-only plugin only to ui', async () => {
109
+ const root = makeTmpRepo({
110
+ configPlugins: ['@forgeportal/plugin-ui-only'],
111
+ workspacePlugins: [{ name: '@forgeportal/plugin-ui-only', type: 'ui' }],
112
+ });
113
+
114
+ await syncCommand({ root, ci: true });
115
+
116
+ expect(readDeps(root, 'api')['@forgeportal/plugin-ui-only']).toBeUndefined();
117
+ expect(readDeps(root, 'ui')['@forgeportal/plugin-ui-only']).toBe('workspace:*');
118
+ });
119
+
120
+ it('removes a plugin no longer in forgeportal.yaml', async () => {
121
+ const root = makeTmpRepo({
122
+ configPlugins: [],
123
+ apiDeps: { '@forgeportal/plugin-old': '^1.0.0' },
124
+ uiDeps: { '@forgeportal/plugin-old': '^1.0.0' },
125
+ });
126
+ // forgeportal.yaml has no plugins now
127
+ fs.writeFileSync(path.join(root, 'forgeportal.yaml'), 'pluginPackages:\n packages: []\n');
128
+
129
+ await syncCommand({ root, ci: true });
130
+
131
+ expect(readDeps(root, 'api')['@forgeportal/plugin-old']).toBeUndefined();
132
+ expect(readDeps(root, 'ui')['@forgeportal/plugin-old']).toBeUndefined();
133
+ });
134
+
135
+ it('does not touch non-plugin deps when removing', async () => {
136
+ const root = makeTmpRepo({
137
+ configPlugins: [],
138
+ apiDeps: {
139
+ 'express': '^4.18.0',
140
+ '@forgeportal/plugin-old': '^1.0.0',
141
+ '@forgeportal/core': '^1.0.0',
142
+ },
143
+ });
144
+ fs.writeFileSync(path.join(root, 'forgeportal.yaml'), 'pluginPackages:\n packages: []\n');
145
+
146
+ await syncCommand({ root, ci: true });
147
+
148
+ const deps = readDeps(root, 'api');
149
+ expect(deps['express']).toBe('^4.18.0');
150
+ expect(deps['@forgeportal/core']).toBe('^1.0.0');
151
+ expect(deps['@forgeportal/plugin-old']).toBeUndefined();
152
+ });
153
+
154
+ it('returns changed:false when already in sync', async () => {
155
+ const root = makeTmpRepo({
156
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
157
+ apiDeps: { '@forgeportal/plugin-kubernetes': 'workspace:*' },
158
+ uiDeps: { '@forgeportal/plugin-kubernetes': 'workspace:*' },
159
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
160
+ });
161
+
162
+ const result = await syncCommand({ root, ci: true });
163
+ expect(result.changed).toBe(false);
164
+ });
165
+ });
166
+
167
+ describe('forge sync --dry-run', () => {
168
+ it('does not modify package.json files', async () => {
169
+ const root = makeTmpRepo({
170
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
171
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
172
+ });
173
+
174
+ const before = JSON.stringify(readDeps(root, 'api'));
175
+ await syncCommand({ root, dryRun: true });
176
+ expect(JSON.stringify(readDeps(root, 'api'))).toBe(before);
177
+ });
178
+
179
+ it('reports changed:true without writing', async () => {
180
+ const root = makeTmpRepo({
181
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
182
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
183
+ });
184
+
185
+ const result = await syncCommand({ root, dryRun: true });
186
+ expect(result.changed).toBe(true);
187
+ expect(readDeps(root, 'api')['@forgeportal/plugin-kubernetes']).toBeUndefined();
188
+ });
189
+ });
190
+
191
+ describe('forge sync --check', () => {
192
+ it('exits 1 when out of sync', async () => {
193
+ const root = makeTmpRepo({
194
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
195
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
196
+ });
197
+
198
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((_code?: number | string | null) => {
199
+ throw new Error(`process.exit(${_code})`);
200
+ });
201
+
202
+ await expect(syncCommand({ root, check: true })).rejects.toThrow('process.exit(1)');
203
+ expect(exitSpy).toHaveBeenCalledWith(1);
204
+ exitSpy.mockRestore();
205
+ });
206
+
207
+ it('exits 0 when in sync', async () => {
208
+ const root = makeTmpRepo({
209
+ configPlugins: ['@forgeportal/plugin-kubernetes'],
210
+ apiDeps: { '@forgeportal/plugin-kubernetes': 'workspace:*' },
211
+ uiDeps: { '@forgeportal/plugin-kubernetes': 'workspace:*' },
212
+ workspacePlugins: [{ name: '@forgeportal/plugin-kubernetes', type: 'fullstack' }],
213
+ });
214
+
215
+ const result = await syncCommand({ root, check: true });
216
+ expect(result.changed).toBe(false);
217
+ });
218
+ });
219
+
220
+ describe('forge sync — no forgeportal.yaml', () => {
221
+ it('returns empty result gracefully when no yaml exists', async () => {
222
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-sync-noconfig-'));
223
+ const result = await syncCommand({ root, ci: true });
224
+ expect(result.changed).toBe(false);
225
+ });
226
+ });
package/src/bin.ts ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * forge — ForgePortal CLI
4
+ *
5
+ * Usage:
6
+ * forge sync # sync plugin deps + pnpm install
7
+ * forge sync --dry-run # show diff, no writes
8
+ * forge sync --ci # write only, no pnpm install (Dockerfile use)
9
+ * forge sync --check # exit 1 if out of sync (CI gate)
10
+ */
11
+ import { syncCommand } from './commands/sync.js';
12
+
13
+ const args = process.argv.slice(2);
14
+ const command = args[0];
15
+
16
+ if (!command || command === '--help' || command === '-h') {
17
+ console.log(`
18
+ forge — ForgePortal CLI
19
+
20
+ Commands:
21
+ sync Sync plugin dependencies from forgeportal.yaml → package.json files
22
+ sync --dry-run Show changes without writing any files
23
+ sync --check Exit 1 if package.json files are out of sync (CI gate)
24
+ sync --ci Write files without running pnpm install (for Dockerfile)
25
+ `);
26
+ process.exit(0);
27
+ }
28
+
29
+ if (command === 'sync') {
30
+ const dryRun = args.includes('--dry-run');
31
+ const ci = args.includes('--ci');
32
+ const check = args.includes('--check');
33
+ const configFlag = args.indexOf('--config');
34
+ const configPath = configFlag !== -1 ? args[configFlag + 1] : undefined;
35
+
36
+ syncCommand({ dryRun, ci, check, configPath }).catch((err: unknown) => {
37
+ console.error('forge sync failed:', err instanceof Error ? err.message : String(err));
38
+ process.exit(1);
39
+ });
40
+ } else {
41
+ console.error(`Unknown command: "${command}". Run "forge --help" for usage.`);
42
+ process.exit(1);
43
+ }
@@ -0,0 +1,323 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
5
+ import yaml from 'js-yaml';
6
+
7
+ // ─── Types ────────────────────────────────────────────────────────────────────
8
+
9
+ export interface SyncOptions {
10
+ dryRun?: boolean;
11
+ ci?: boolean;
12
+ check?: boolean;
13
+ configPath?: string;
14
+ /** Override the monorepo root (useful for tests). Defaults to process.cwd(). */
15
+ root?: string;
16
+ }
17
+
18
+ export interface SyncResult {
19
+ apiAdded: string[];
20
+ apiRemoved: string[];
21
+ uiAdded: string[];
22
+ uiRemoved: string[];
23
+ changed: boolean;
24
+ }
25
+
26
+ interface ForgePortalConfig {
27
+ pluginPackages?: { packages?: string[] };
28
+ }
29
+
30
+ interface PackageJson {
31
+ dependencies?: Record<string, string>;
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ interface PluginInfo {
36
+ packageName: string;
37
+ version: string;
38
+ pluginType: 'ui' | 'backend' | 'fullstack';
39
+ }
40
+
41
+ // ─── Main command ─────────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ * `forge sync` — synchronises plugin dependencies from forgeportal.yaml
45
+ * into apps/api/package.json and apps/ui/package.json.
46
+ *
47
+ * Modes:
48
+ * default write + pnpm install
49
+ * --dry-run print diff, no writes
50
+ * --check exit 1 if out of sync (CI gate)
51
+ * --ci write files, skip pnpm install (Dockerfile use)
52
+ */
53
+ export async function syncCommand(opts: SyncOptions = {}): Promise<SyncResult> {
54
+ const root = opts.root ?? process.cwd();
55
+ const cfgPath = opts.configPath ?? path.join(root, 'forgeportal.yaml');
56
+
57
+ // ── 1. Read forgeportal.yaml ──────────────────────────────────────────────
58
+
59
+ if (!fs.existsSync(cfgPath)) {
60
+ if (opts.ci) {
61
+ console.log('[forge sync] No forgeportal.yaml found — nothing to sync.');
62
+ return emptyResult();
63
+ }
64
+ console.warn(`[forge sync] Warning: ${cfgPath} not found. Nothing to sync.`);
65
+ return emptyResult();
66
+ }
67
+
68
+ const config = yaml.load(fs.readFileSync(cfgPath, 'utf8')) as ForgePortalConfig;
69
+ const packages = config.pluginPackages?.packages ?? [];
70
+
71
+ // ── 2. Resolve manifests ──────────────────────────────────────────────────
72
+
73
+ if (packages.length === 0) {
74
+ console.log('[forge sync] No plugins configured — will remove any installed forge plugins.');
75
+ }
76
+
77
+ const resolved: PluginInfo[] = [];
78
+ for (const pkg of packages) {
79
+ try {
80
+ const info = await resolveManifest(pkg, root);
81
+ resolved.push(info);
82
+ } catch (err) {
83
+ const msg = err instanceof Error ? err.message : String(err);
84
+ if (opts.ci) {
85
+ console.error(`[forge sync] ERROR: Cannot resolve manifest for "${pkg}": ${msg}`);
86
+ process.exit(1);
87
+ }
88
+ console.warn(`[forge sync] Warning: Cannot resolve manifest for "${pkg}": ${msg}. Skipping.`);
89
+ }
90
+ }
91
+
92
+ // ── 3. Build desired dep maps ─────────────────────────────────────────────
93
+
94
+ const apiDeps: Record<string, string> = {};
95
+ const uiDeps: Record<string, string> = {};
96
+
97
+ for (const { packageName, version, pluginType } of resolved) {
98
+ const versionSpec = version.startsWith('workspace:') ? version : `^${version}`;
99
+ if (pluginType === 'backend' || pluginType === 'fullstack') apiDeps[packageName] = versionSpec;
100
+ if (pluginType === 'ui' || pluginType === 'fullstack') uiDeps[packageName] = versionSpec;
101
+ }
102
+
103
+ // ── 4. Apply to package.json files ────────────────────────────────────────
104
+
105
+ const apiPkgPath = path.join(root, 'apps', 'api', 'package.json');
106
+ const uiPkgPath = path.join(root, 'apps', 'ui', 'package.json');
107
+
108
+ const apiDiff = computeDiff(apiPkgPath, apiDeps);
109
+ const uiDiff = computeDiff(uiPkgPath, uiDeps);
110
+ const changed = apiDiff.changed || uiDiff.changed;
111
+
112
+ // ── 5. Handle modes ───────────────────────────────────────────────────────
113
+
114
+ if (opts.check) {
115
+ if (changed) {
116
+ console.error(
117
+ '✗ package.json files are out of sync with forgeportal.yaml.\n' +
118
+ ' Run "pnpm forge:sync" to fix.\n',
119
+ );
120
+ printDiff(apiPkgPath, apiDiff);
121
+ printDiff(uiPkgPath, uiDiff);
122
+ process.exit(1);
123
+ }
124
+ console.log('✓ package.json files are in sync with forgeportal.yaml.');
125
+ return toResult(apiDiff, uiDiff);
126
+ }
127
+
128
+ if (opts.dryRun) {
129
+ console.log('[forge sync --dry-run] Changes that would be applied:\n');
130
+ printDiff(apiPkgPath, apiDiff);
131
+ printDiff(uiPkgPath, uiDiff);
132
+ if (!changed) console.log(' (no changes needed)');
133
+ return toResult(apiDiff, uiDiff);
134
+ }
135
+
136
+ if (!changed) {
137
+ console.log('✓ package.json files already in sync — nothing to do.');
138
+ return emptyResult();
139
+ }
140
+
141
+ // Write files
142
+ applyDiff(apiPkgPath, apiDiff);
143
+ applyDiff(uiPkgPath, uiDiff);
144
+
145
+ const totalAdded = apiDiff.toAdd.length + uiDiff.toAdd.length;
146
+ const totalRemoved = apiDiff.toRemove.length + uiDiff.toRemove.length;
147
+ console.log(
148
+ `✓ Synced: +${totalAdded} dep(s) added, -${totalRemoved} dep(s) removed across api/ui.`,
149
+ );
150
+
151
+ // ── 6. pnpm install (skip in --ci) ────────────────────────────────────────
152
+
153
+ if (!opts.ci) {
154
+ console.log(' Running pnpm install…');
155
+ execSync('pnpm install', { stdio: 'inherit', cwd: root });
156
+ console.log('✓ pnpm install complete.');
157
+ }
158
+
159
+ return toResult(apiDiff, uiDiff);
160
+ }
161
+
162
+ // ─── Manifest resolution ──────────────────────────────────────────────────────
163
+
164
+ /**
165
+ * Resolves a plugin manifest from the workspace, node_modules, or the npm registry.
166
+ * Workspace packages always take priority and receive the "workspace:*" version specifier.
167
+ */
168
+ async function resolveManifest(packageName: string, root: string): Promise<PluginInfo> {
169
+ // ── 1. Workspace scan: packages directory ─────────────────────────────────
170
+ const pkgsDir = path.join(root, 'packages');
171
+ if (fs.existsSync(pkgsDir)) {
172
+ for (const dir of fs.readdirSync(pkgsDir)) {
173
+ const pkgJsonPath = path.join(pkgsDir, dir, 'package.json');
174
+ const manifestPath = path.join(pkgsDir, dir, 'forgeportal-plugin.json');
175
+ if (!fs.existsSync(pkgJsonPath) || !fs.existsSync(manifestPath)) continue;
176
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) as { name?: string };
177
+ if (pkg.name !== packageName) continue;
178
+ const manifest = JSON.parse(
179
+ fs.readFileSync(manifestPath, 'utf8'),
180
+ ) as { version: string; forgeportal: { type: string } };
181
+ return {
182
+ packageName,
183
+ version: 'workspace:*',
184
+ pluginType: manifest.forgeportal.type as PluginInfo['pluginType'],
185
+ };
186
+ }
187
+ }
188
+
189
+ // ── 2. node_modules via require.resolve (external installed package) ───────
190
+ try {
191
+ const req = createRequire(path.join(root, '_placeholder_.js'));
192
+ const pkgDir = path.dirname(req.resolve(`${packageName}/package.json`));
193
+ const manifest = JSON.parse(
194
+ fs.readFileSync(path.join(pkgDir, 'forgeportal-plugin.json'), 'utf8'),
195
+ ) as { version: string; forgeportal: { type: string } };
196
+
197
+ // If the resolved package lives under any "packages/" directory (workspace
198
+ // symlink or monorepo root), always use "workspace:*" so pnpm is happy.
199
+ const normalizedPkgDir = pkgDir.replace(/\\/g, '/');
200
+ const isWorkspaceLink = [root, process.cwd()].some((base) =>
201
+ normalizedPkgDir.startsWith(path.join(base, 'packages').replace(/\\/g, '/')),
202
+ );
203
+
204
+ return {
205
+ packageName,
206
+ version: isWorkspaceLink ? 'workspace:*' : manifest.version,
207
+ pluginType: manifest.forgeportal.type as PluginInfo['pluginType'],
208
+ };
209
+ } catch {
210
+ // not installed in node_modules — try npm registry
211
+ }
212
+
213
+ // ── 3. Fall back to npm registry ──────────────────────────────────────────
214
+ try {
215
+ const raw = execSync(
216
+ `npm view ${packageName} version --json 2>/dev/null`,
217
+ { encoding: 'utf8', timeout: 15_000 },
218
+ ).trim();
219
+ const version = JSON.parse(raw) as string;
220
+
221
+ // Fetch forgeportal field to determine type
222
+ const meta = execSync(
223
+ `npm view ${packageName} forgeportal --json 2>/dev/null`,
224
+ { encoding: 'utf8', timeout: 15_000 },
225
+ ).trim();
226
+ const forgeportal = JSON.parse(meta) as { type?: string } | null;
227
+ const pluginType = (forgeportal?.type ?? 'backend') as PluginInfo['pluginType'];
228
+
229
+ return { packageName, version, pluginType };
230
+ } catch {
231
+ throw new Error(
232
+ `Plugin "${packageName}" not found in workspace packages, node_modules, or npm registry.`,
233
+ );
234
+ }
235
+ }
236
+
237
+ // ─── Diff helpers ─────────────────────────────────────────────────────────────
238
+
239
+ interface Diff {
240
+ changed: boolean;
241
+ toAdd: Array<[string, string]>; // [name, version]
242
+ toRemove: string[];
243
+ }
244
+
245
+ /**
246
+ * Computes the diff between current deps in package.json and desired deps.
247
+ * Also removes plugin deps that are no longer configured.
248
+ */
249
+ function computeDiff(pkgPath: string, desiredDeps: Record<string, string>): Diff {
250
+ if (!fs.existsSync(pkgPath)) {
251
+ return { changed: false, toAdd: [], toRemove: [] };
252
+ }
253
+
254
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as PackageJson;
255
+ const current = pkg.dependencies ?? {};
256
+
257
+ const toRemove = Object.keys(current).filter(
258
+ (dep) => isPluginPackage(dep) && !(dep in desiredDeps),
259
+ );
260
+ const toAdd = Object.entries(desiredDeps).filter(
261
+ ([dep, ver]) => current[dep] !== ver,
262
+ );
263
+
264
+ return {
265
+ changed: toRemove.length > 0 || toAdd.length > 0,
266
+ toAdd,
267
+ toRemove,
268
+ };
269
+ }
270
+
271
+ function applyDiff(pkgPath: string, diff: Diff): void {
272
+ if (!diff.changed || !fs.existsSync(pkgPath)) return;
273
+
274
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as PackageJson;
275
+ const current = pkg.dependencies ?? {};
276
+
277
+ for (const dep of diff.toRemove) delete current[dep];
278
+ for (const [dep, ver] of diff.toAdd) current[dep] = ver;
279
+
280
+ pkg.dependencies = sortKeys(current);
281
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
282
+
283
+ const label = path.relative(process.cwd(), pkgPath);
284
+ console.log(` ✓ ${label}: +${diff.toAdd.length} -${diff.toRemove.length}`);
285
+ }
286
+
287
+ function printDiff(pkgPath: string, diff: Diff): void {
288
+ if (!diff.changed) return;
289
+ const label = path.relative(process.cwd(), pkgPath);
290
+ console.log(` ${label}:`);
291
+ for (const [dep, ver] of diff.toAdd) console.log(` + ${dep}@${ver}`);
292
+ for (const dep of diff.toRemove) console.log(` - ${dep}`);
293
+ }
294
+
295
+ function sortKeys(obj: Record<string, string>): Record<string, string> {
296
+ return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
297
+ }
298
+
299
+ /**
300
+ * Returns true if the package is a user-installable ForgePortal plugin
301
+ * managed by `forge sync`. Excludes framework packages like @forgeportal/plugin-sdk.
302
+ */
303
+ function isPluginPackage(name: string): boolean {
304
+ // plugin-sdk is a framework/SDK package — never remove it automatically.
305
+ if (name === '@forgeportal/plugin-sdk') return false;
306
+ return name.startsWith('@forgeportal/plugin-') || /^forge-plugin-/.test(name) || /^@[^/]+\/forge-plugin-/.test(name);
307
+ }
308
+
309
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
310
+
311
+ function emptyResult(): SyncResult {
312
+ return { apiAdded: [], apiRemoved: [], uiAdded: [], uiRemoved: [], changed: false };
313
+ }
314
+
315
+ function toResult(apiDiff: Diff, uiDiff: Diff): SyncResult {
316
+ return {
317
+ apiAdded: apiDiff.toAdd.map(([d]) => d),
318
+ apiRemoved: apiDiff.toRemove,
319
+ uiAdded: uiDiff.toAdd.map(([d]) => d),
320
+ uiRemoved: uiDiff.toRemove,
321
+ changed: apiDiff.changed || uiDiff.changed,
322
+ };
323
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { syncCommand } from './commands/sync.js';
2
+ export type { SyncOptions, SyncResult } from './commands/sync.js';
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "declaration": true,
7
+ "declarationMap": true
8
+ },
9
+ "include": ["src"]
10
+ }