Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ npx @flisk/analyze-tracking /path/to/project [options]
- `-m, --model <model>`: Specify a model (ex: `gpt-4.1-nano`, `gpt-4o-mini`, `gemini-2.0-flash-lite-001`)
- `-o, --output <output_file>`: Name of the output file (default: `tracking-schema.yaml`)
- `-c, --customFunction <function_name>`: Specify a custom tracking function
- `--format <format>`: Output format, either `yaml` (default) or `json`. If an invalid value is provided, the CLI will exit with an error.
- `--stdout`: Print the output to the terminal instead of writing to a file (works with both YAML and JSON)

🔑&nbsp; **Important:** If you are using `generateDescription`, you must set the appropriate credentials for the LLM provider you are using as an environment variable. OpenAI uses `OPENAI_API_KEY` and Google Vertex AI uses `GOOGLE_APPLICATION_CREDENTIALS`.

Expand Down Expand Up @@ -416,3 +418,30 @@ See [schema.json](schema.json) for a JSON Schema of the output.

## Contribute
We're actively improving this package. Found a bug? Have a feature request? Open an issue or submit a pull request!

#### Examples

Output YAML to a file (default):
```sh
npx @flisk/analyze-tracking /path/to/project
```

Output JSON to a file:
```sh
npx @flisk/analyze-tracking /path/to/project --format json --output tracking-schema.json
```

Print YAML to the terminal:
```sh
npx @flisk/analyze-tracking /path/to/project --stdout
```

Print JSON to the terminal:
```sh
npx @flisk/analyze-tracking /path/to/project --format json --stdout
```

If you provide an invalid format (e.g., `--format xml`), the CLI will print an error and exit:
```
Invalid format: xml. Please use --format yaml or --format json.
```
30 changes: 29 additions & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ const optionDefinitions = [
alias: 'h',
type: Boolean,
},
{
name: 'stdout',
type: Boolean,
defaultValue: false,
},
{
name: 'format',
alias: 'f',
type: String,
defaultValue: 'yaml',
},
]
const options = commandLineArgs(optionDefinitions);
const {
Expand All @@ -77,6 +88,8 @@ const {
commitHash,
commitTimestamp,
help,
stdout,
format,
} = options;

if (help) {
Expand Down Expand Up @@ -113,4 +126,19 @@ if (generateDescription) {
}
}

run(path.resolve(targetDir), output, customFunction, customSourceDetails, generateDescription, provider, model);
if (format !== 'yaml' && format !== 'json') {
console.error(`Invalid format: ${format}. Please use --format yaml or --format json.`);
process.exit(1);
}

run(
path.resolve(targetDir),
output,
customFunction,
customSourceDetails,
generateDescription,
provider,
model,
stdout,
format
);
10 changes: 7 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

const { analyzeDirectory } = require('./analyze');
const { getRepoDetails } = require('./utils/repoDetails');
const { generateYamlSchema } = require('./utils/yamlGenerator');
const { generateYamlSchema, generateJsonSchema } = require('./utils/yamlGenerator');
const { generateDescriptions } = require('./generateDescriptions');

const { ChatOpenAI } = require('@langchain/openai');
const { ChatVertexAI } = require('@langchain/google-vertexai');

async function run(targetDir, outputPath, customFunction, customSourceDetails, generateDescription, provider, model) {
async function run(targetDir, outputPath, customFunction, customSourceDetails, generateDescription, provider, model, stdout, format) {
let events = await analyzeDirectory(targetDir, customFunction);
if (generateDescription) {
let llm;
Expand All @@ -35,7 +35,11 @@ async function run(targetDir, outputPath, customFunction, customSourceDetails, g
events = await generateDescriptions(events, targetDir, llm);
}
const repoDetails = await getRepoDetails(targetDir, customSourceDetails);
generateYamlSchema(events, repoDetails, outputPath);
if (format === 'json') {
generateJsonSchema(events, repoDetails, outputPath, stdout);
} else {
generateYamlSchema(events, repoDetails, outputPath, stdout);
}
}

module.exports = { run };
27 changes: 23 additions & 4 deletions src/utils/yamlGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const yaml = require('js-yaml');
const VERSION = 1
const SCHEMA_URL = "https://raw.githubusercontent.com/fliskdata/analyze-tracking/main/schema.json";

function generateYamlSchema(events, repository, outputPath) {
function generateYamlSchema(events, repository, outputPath, stdout = false) {
const schema = {
version: VERSION,
source: repository,
Expand All @@ -21,8 +21,27 @@ function generateYamlSchema(events, repository, outputPath) {
};
const yamlOutput = yaml.dump(schema, options);
const yamlFile = `# yaml-language-server: $schema=${SCHEMA_URL}\n${yamlOutput}`;
fs.writeFileSync(outputPath, yamlFile, 'utf8');
console.log(`Tracking schema YAML file generated: ${outputPath}`);
if (stdout) {
process.stdout.write(yamlFile);
} else {
fs.writeFileSync(outputPath, yamlFile, 'utf8');
console.log(`Tracking schema YAML file generated: ${outputPath}`);
}
}

module.exports = { generateYamlSchema };
function generateJsonSchema(events, repository, outputPath, stdout = false) {
const schema = {
version: VERSION,
source: repository,
events,
};
const jsonFile = JSON.stringify(schema, null, 2);
if (stdout) {
process.stdout.write(jsonFile);
} else {
fs.writeFileSync(outputPath, jsonFile, 'utf8');
console.log(`Tracking schema JSON file generated: ${outputPath}`);
}
}

module.exports = { generateYamlSchema, generateJsonSchema };
119 changes: 119 additions & 0 deletions tests/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,123 @@ test.describe('CLI End-to-End Tests', () => {
// Compare YAML files
compareYAMLFiles(outputFile, expectedFile);
});

test('should print YAML to stdout when --stdout is used', async () => {
const targetDir = path.join(fixturesDir, 'javascript');
const expectedFile = path.join(fixturesDir, 'javascript', 'tracking-schema-javascript.yaml');
const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "customTrackFunction" --stdout`;
let stdout;
try {
stdout = execSync(command, { encoding: 'utf8' });
} catch (error) {
assert.fail(`CLI command failed: ${error.message}`);
}
// Remove the YAML language server comment from both outputs
const actualYAML = stdout.replace(/^# yaml-language-server:.*\n/, '');
const expectedYAML = fs.readFileSync(expectedFile, 'utf8').replace(/^# yaml-language-server:.*\n/, '');
// Parse YAML
const actual = yaml.load(actualYAML);
const expected = yaml.load(expectedYAML);
// Compare version
assert.strictEqual(actual.version, expected.version);
// Compare source (ignoring dynamic fields like commit and timestamp)
assert.ok(actual.source);
assert.ok(actual.source.repository);
// Compare events using deep equality (order-insensitive)
assert.deepStrictEqual(actual.events, expected.events);
});

test('should not print output file message when --stdout is used', async () => {
const targetDir = path.join(fixturesDir, 'javascript');
const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "customTrackFunction" --stdout`;
let stdout;
try {
stdout = execSync(command, { encoding: 'utf8' });
} catch (error) {
assert.fail(`CLI command failed: ${error.message}`);
}
// Ensure the output does not contain the file generated message
assert.ok(!stdout.includes('Tracking schema YAML file generated'), 'Should not print output file message when using --stdout');
});

test('should print JSON to stdout when --format json is used', async () => {
const targetDir = path.join(fixturesDir, 'javascript');
const expectedFile = path.join(fixturesDir, 'javascript', 'tracking-schema-javascript.yaml');
const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "customTrackFunction" --stdout --format json`;
let stdout;
try {
stdout = execSync(command, { encoding: 'utf8' });
} catch (error) {
assert.fail(`CLI command failed: ${error.message}`);
}
// Should not contain YAML language server comment
assert.ok(!stdout.includes('# yaml-language-server'), 'Should not contain YAML language server comment in JSON output');
// Should not contain file output message
assert.ok(!stdout.includes('Tracking schema YAML file generated'), 'Should not print output file message when using --stdout and --format json');
// Should be valid JSON
let actual;
try {
actual = JSON.parse(stdout);
} catch (e) {
assert.fail('Output is not valid JSON');
}
// Compare to expected YAML fixture loaded as JS object
const expectedYAML = fs.readFileSync(expectedFile, 'utf8').replace(/^# yaml-language-server:.*\n/, '');
const expected = yaml.load(expectedYAML);
// Compare version
assert.strictEqual(actual.version, expected.version);
// Compare source (ignoring dynamic fields like commit and timestamp)
assert.ok(actual.source);
assert.ok(actual.source.repository);
// Compare events using deep equality (order-insensitive)
assert.deepStrictEqual(actual.events, expected.events);
});

test('should write JSON file when --format json is used without --stdout', async () => {
const targetDir = path.join(fixturesDir, 'javascript');
const outputFile = path.join(tempDir, 'tracking-schema-javascript-test.json');
const expectedFile = path.join(fixturesDir, 'javascript', 'tracking-schema-javascript.yaml');
const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "customTrackFunction" --output "${outputFile}" --format json`;
let stdout;
try {
stdout = execSync(command, { encoding: 'utf8' });
} catch (error) {
assert.fail(`CLI command failed: ${error.message}`);
}
// Should print output file message
assert.ok(stdout.includes('Tracking schema YAML file generated') || stdout.includes('Tracking schema JSON file generated'), 'Should print output file message');
// Check output file exists
assert.ok(fs.existsSync(outputFile), 'Output file should be created');
// Should be valid JSON
let actual;
try {
actual = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
} catch (e) {
assert.fail('Output file is not valid JSON');
}
// Compare to expected YAML fixture loaded as JS object
const expectedYAML = fs.readFileSync(expectedFile, 'utf8').replace(/^# yaml-language-server:.*\n/, '');
const expected = yaml.load(expectedYAML);
// Compare version
assert.strictEqual(actual.version, expected.version);
// Compare source (ignoring dynamic fields like commit and timestamp)
assert.ok(actual.source);
assert.ok(actual.source.repository);
// Compare events using deep equality (order-insensitive)
assert.deepStrictEqual(actual.events, expected.events);
});

test('should fail with a clear error if --format is not yaml or json', async () => {
const targetDir = path.join(fixturesDir, 'javascript');
const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "customTrackFunction" --stdout --format xml`;
let errorCaught = false;
try {
execSync(command, { encoding: 'utf8', stdio: 'pipe' });
} catch (error) {
errorCaught = true;
assert.ok(error.stderr.includes('Invalid format'), 'Should mention invalid format');
assert.ok(error.stderr.match(/yaml|json/), 'Should mention yaml or json as valid options');
}
assert.ok(errorCaught, 'CLI should fail for invalid format');
});
});