Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7e64cb5
feat(cli): initial work on bundles
willosborne Jan 26, 2026
1d60050
feat(cli): add initial calm dev commands
willosborne Jan 27, 2026
e61cd67
feat(cli): add simple pull command
willosborne Jan 27, 2026
33ef45f
feat(cli): add calm dev tree command to output bundle deps
willosborne Jan 28, 2026
3013c30
refactor(cli): rename dev commands to workspace commands
willosborne Jan 28, 2026
dc1cdaa
feat(cli): add reference mode for calm workspace add
willosborne Jan 28, 2026
17d4c7a
feat(cli): add list, switch and show commands
willosborne Jan 28, 2026
74a3e7b
feat(cli): add clean command and improve logging
willosborne Jan 29, 2026
0b41e8d
feat(cli): make init take positional arg
willosborne Jan 29, 2026
2ef8ac3
feat(cli): properly initialise loader and add scratch notes
willosborne Jan 30, 2026
8193ca2
feat(cli): fixes and improvements
willosborne Jan 30, 2026
0186dfc
fix(cli): suppress unneeded logging from pull
willosborne Jan 30, 2026
426d270
feat(cli): filter for other types of file ref
willosborne Jan 30, 2026
8dc8b16
feat(cli): update notes
willosborne Jan 30, 2026
08aa97e
feat(cli): gitignore .calm-workspace
willosborne Feb 2, 2026
72b4477
feat(cli): improve file structure, tests and load urls from patterns too
willosborne Feb 2, 2026
805a2a8
feat(cli): resolve $schema too
willosborne Feb 2, 2026
4ab37e3
feat(cli): improve structure and tests
willosborne Feb 4, 2026
b87b569
feat(cli): add notes about how to load resources
willosborne Feb 4, 2026
5c858e5
feat(cli): remove notes
willosborne Feb 5, 2026
7039296
feat(cli): fix tests
willosborne Feb 5, 2026
8f63035
feat(calm-hub-ui): fix tests on node 25
willosborne Feb 5, 2026
75cf7f1
feat(cli): fix linting
willosborne Feb 5, 2026
fc2dc91
feat(cli): fix lockfile and package.json errors
willosborne Feb 5, 2026
6309ffb
feat(cli): update tests and remove dead code
willosborne Feb 5, 2026
2e28c3e
Merge branch 'main' into bundle-file-loader
rocketstack-matt Feb 7, 2026
a352931
chore: resolve merge conflict in package-lock.json from upstream/main
rocketstack-matt Feb 9, 2026
36dcbfe
Merge branch 'main' into bundle-file-loader
rocketstack-matt Feb 12, 2026
a30c3cd
fix(cli): fix lockfile
willosborne Feb 16, 2026
2b98a21
Merge branch 'main' into bundle-file-loader
willosborne Feb 16, 2026
451b10d
feat(cli): remove notes
willosborne Feb 16, 2026
442ee68
Merge branch 'main' into bundle-file-loader
willosborne Feb 26, 2026
f4713ec
feat(cli): pr feedback
willosborne Feb 26, 2026
f2eb989
Merge branch 'main' into bundle-file-loader
willosborne Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ coverage/
.github/chatmodes
.github/agents
shared/test-output/
.calm-workspace/
cli/test_fixtures/getting-started/actual-output/
23 changes: 20 additions & 3 deletions calm-hub-ui/vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,28 @@ import '@testing-library/jest-dom/vitest';
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';

// Node.js 25+ defines a non-functional localStorage on globalThis that
// shadows jsdom's implementation. Override it with a proper Storage polyfill.
if (typeof globalThis.localStorage === 'object' && typeof globalThis.localStorage.clear !== 'function') {
const storage = new Map<string, string>();
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
enumerable: true,
value: {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => storage.set(key, String(value)),
removeItem: (key: string) => storage.delete(key),
clear: () => storage.clear(),
get length() { return storage.size; },
key: (index: number) => [...storage.keys()][index] ?? null,
},
});
}
// Polyfill ResizeObserver for ReactFlow tests
global.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
observe() { }
unobserve() { }
disconnect() { }
};

// runs a clean after each test case (e.g. clearing jsdom)
Expand Down
1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"execa": "^9.6.0",
"express-rate-limit": "^8.0.0",
"mkdirp": "^3.0.1",
"tree-dump": "^1.1.0",
"ts-node": "10.9.2"
},
"devDependencies": {
Expand Down
25 changes: 21 additions & 4 deletions cli/src/cli.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
CALM_META_SCHEMA_DIRECTORY,
Docifier,
DocifyMode,
TemplateProcessingMode,
Expand All @@ -15,6 +14,7 @@ let serverModule: typeof import('./server/cli-server');
let templateModule: typeof import('./command-helpers/template');
let optionsModule: typeof import('./command-helpers/generate-options');
let fileSystemDocLoaderModule: typeof import('@finos/calm-shared/dist/document-loader/file-system-document-loader');
let documentLoaderModule: typeof import('../../shared/src/document-loader/document-loader');
let setupCLI: typeof import('./cli').setupCLI;
let cliConfigModule: typeof import('./cli-config');

Expand All @@ -31,6 +31,7 @@ describe('CLI Commands', () => {
templateModule = await import('./command-helpers/template');
optionsModule = await import('./command-helpers/generate-options');
fileSystemDocLoaderModule = await import('@finos/calm-shared/dist/document-loader/file-system-document-loader');
documentLoaderModule = await import('../../shared/src/document-loader/document-loader');

vi.spyOn(calmShared, 'runGenerate').mockResolvedValue(undefined);
vi.spyOn(calmShared.TemplateProcessor.prototype, 'processTemplate').mockResolvedValue(undefined);
Expand All @@ -47,6 +48,16 @@ describe('CLI Commands', () => {
vi.spyOn(fileSystemDocLoaderModule, 'FileSystemDocumentLoader').mockImplementation(vi.fn());
vi.spyOn(fileSystemDocLoaderModule.FileSystemDocumentLoader.prototype, 'loadMissingDocument').mockResolvedValue({});

// Mock buildDocumentLoader to return a mock DocumentLoader.
// The generate command now uses buildDocumentLoader() which creates a
// MultiStrategyDocumentLoader internally. We mock it to return a simple
// loader whose loadMissingDocument resolves with an empty object.
vi.spyOn(documentLoaderModule, 'buildDocumentLoader').mockReturnValue({
initialise: vi.fn().mockResolvedValue(undefined),
loadMissingDocument: vi.fn().mockResolvedValue({}),
resolvePath: vi.fn().mockReturnValue(undefined),
});

const cliModule = await import('./cli');
setupCLI = cliModule.setupCLI;

Expand All @@ -64,10 +75,16 @@ describe('CLI Commands', () => {
'--schema-directory', 'schemas',
]);

expect(fileSystemDocLoaderModule.FileSystemDocumentLoader.prototype.loadMissingDocument).toHaveBeenCalledWith('pattern.json', 'pattern');
expect(optionsModule.promptUserForOptions).toHaveBeenCalled();
expect(documentLoaderModule.buildDocumentLoader).toHaveBeenCalledWith(
expect.objectContaining({
schemaDirectoryPath: 'schemas',
debug: true,
})
);

expect(fileSystemDocLoaderModule.FileSystemDocumentLoader).toHaveBeenCalledWith([CALM_META_SCHEMA_DIRECTORY, 'schemas'], true, process.cwd());
const mockDocLoader = vi.mocked(documentLoaderModule.buildDocumentLoader).mock.results[0].value;
expect(mockDocLoader.loadMissingDocument).toHaveBeenCalledWith('pattern.json', 'pattern');
expect(optionsModule.promptUserForOptions).toHaveBeenCalled();

expect(calmShared.runGenerate).toHaveBeenCalledWith(
{}, 'output.json', true, expect.any(calmShared.SchemaDirectory), []
Expand Down
40 changes: 39 additions & 1 deletion cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { Option, Command } from 'commander';
import { version } from '../package.json';
import { promptUserForOptions } from './command-helpers/generate-options';
import { CalmChoice } from '@finos/calm-shared/dist/commands/generate/components/options';
import { buildDocumentLoader, DocumentLoader, DocumentLoaderOptions } from '@finos/calm-shared/dist/document-loader/document-loader';
import { buildDocumentLoader, DocumentLoader, DocumentLoaderOptions } from '../../shared/src/document-loader/document-loader';
import { loadCliConfig } from './cli-config';
import path from 'path';
import inquirer from 'inquirer';
import { findWorkspaceBundlePath } from './workspace-resolver';
import { setupWorkspaceCommands } from './command-helpers/workspace/commands';
import { loadManifest } from './command-helpers/workspace/bundle';

// Shared options used across multiple commands
const ARCHITECTURE_OPTION = '-a, --architecture <file>';
Expand Down Expand Up @@ -255,6 +258,8 @@ Validation requires:
await setupAiTools(selectedProvider, options.directory, !!options.verbose);
});

// Dev commands
setupWorkspaceCommands(program);
}

interface ParseDocumentLoaderOptions {
Expand Down Expand Up @@ -282,6 +287,39 @@ export async function parseDocumentLoaderConfig(
logger.info('Using CALMHub URL from config file: ' + userConfig.calmHubUrl);
docLoaderOpts.calmHubUrl = userConfig.calmHubUrl;
}

// If a CALM workspace bundle is present in the repository, prefer it for resolving documents
try {
const workspaceBundle = findWorkspaceBundlePath(process.cwd());
if (workspaceBundle) {
logger.info('Using workspace bundle for document resolution: ' + workspaceBundle);
// Load the bundle manifest and construct a URL->local file map from it
try {
const manifest = await loadManifest(workspaceBundle);
const bundleMap = new Map<string, string>();
for (const [id, rel] of Object.entries(manifest)) {
// manifest values are relative to bundlePath
bundleMap.set(id, path.resolve(workspaceBundle, rel));
}

// Merge with any provided urlToLocalMap, allowing bundle entries to override
const combined = new Map<string, string>(urlToLocalMap ?? []);
for (const [k, v] of bundleMap.entries()) {
combined.set(k, v);
}

docLoaderOpts.urlToLocalMap = combined;

// Ensure basePath is set so MappedDocumentLoader can resolve relative mappings if needed
docLoaderOpts.basePath = docLoaderOpts.basePath ?? workspaceBundle;
} catch (err) {
logger.debug('Failed to load workspace bundle manifest: ' + (err instanceof Error ? err.message : String(err)));
}
}
} catch (err) {
logger.debug('Error while checking for workspace bundle: ' + (err instanceof Error ? err.message : String(err)));
}

return docLoaderOpts;
}

Expand Down
Loading
Loading