diff --git a/.changeset/hungry-lights-love.md b/.changeset/hungry-lights-love.md new file mode 100644 index 000000000..232d38d53 --- /dev/null +++ b/.changeset/hungry-lights-love.md @@ -0,0 +1,5 @@ +--- +'@openfn/lexicon': patch +--- + +Allow project.alias to be null diff --git a/.changeset/late-needles-scream.md b/.changeset/late-needles-scream.md new file mode 100644 index 000000000..5b48afa21 --- /dev/null +++ b/.changeset/late-needles-scream.md @@ -0,0 +1,5 @@ +--- +'@openfn/cli': patch +--- + +Fix an issue on checkout where incorrect divergence warnings can be shown diff --git a/.changeset/yummy-balloons-search.md b/.changeset/yummy-balloons-search.md new file mode 100644 index 000000000..c6cac63f2 --- /dev/null +++ b/.changeset/yummy-balloons-search.md @@ -0,0 +1,5 @@ +--- +'@openfn/project': patch +--- + +Set the correct alias on the checked out project diff --git a/integration-tests/cli/test/errors.test.ts b/integration-tests/cli/test/errors.test.ts index 666026c8b..315722079 100644 --- a/integration-tests/cli/test/errors.test.ts +++ b/integration-tests/cli/test/errors.test.ts @@ -22,7 +22,6 @@ test.serial('expression not found', async (t) => { const stdlogs = extractLogs(stdout); assertLog(t, stdlogs, /expression not found/i); assertLog(t, stdlogs, /failed to load the expression from blah.js/i); - assertLog(t, stdlogs, /critical error: aborting command/i); }); test.serial('workflow not found', async (t) => { @@ -33,7 +32,6 @@ test.serial('workflow not found', async (t) => { assertLog(t, stdlogs, /workflow not found/i); assertLog(t, stdlogs, /failed to load a workflow from blah.json/i); - assertLog(t, stdlogs, /critical error: aborting command/i); }); test.serial('job contains invalid js', async (t) => { @@ -45,7 +43,6 @@ test.serial('job contains invalid js', async (t) => { assertLog(t, stdlogs, /failed to compile job/i); assertLog(t, stdlogs, /unexpected token \(2:10\)/i); assertLog(t, stdlogs, /check the syntax of the job expression/i); - assertLog(t, stdlogs, /critical error: aborting command/i); }); // TODO this should really mention which job threw the error @@ -60,7 +57,6 @@ test.serial('workflow references a job with invalid js', async (t) => { assertLog(t, stdlogs, /failed to compile job/i); assertLog(t, stdlogs, /unexpected token \(2:10\)/i); assertLog(t, stdlogs, /check the syntax of the job expression/i); - assertLog(t, stdlogs, /critical error: aborting command/i); }); test.serial("can't find an expression referenced in a workflow", async (t) => { @@ -77,7 +73,6 @@ test.serial("can't find an expression referenced in a workflow", async (t) => { stdlogs, /This workflow references a file which cannot be found at does-not-exist.js/i ); - assertLog(t, stdlogs, /critical error: aborting command/i); }); test.serial("can't find config referenced in a workflow", async (t) => { @@ -98,7 +93,6 @@ test.serial("can't find config referenced in a workflow", async (t) => { stdlogs, /This workflow references a file which cannot be found at does-not-exist.js/i ); - assertLog(t, stdlogs, /critical error: aborting command/i); }); test.serial('circular workflow', async (t) => { @@ -141,7 +135,6 @@ test.serial('invalid end (ambiguous)', async (t) => { const stdlogs = extractLogs(stdout); assertLog(t, stdlogs, /Error: end pattern matched multiple steps/i); - assertLog(t, stdlogs, /aborting/i); }); // These test error outputs within valid workflows diff --git a/integration-tests/cli/test/project-v1.test.ts b/integration-tests/cli/test/project-v1.test.ts index 7f60a315b..3865d6a54 100644 --- a/integration-tests/cli/test/project-v1.test.ts +++ b/integration-tests/cli/test/project-v1.test.ts @@ -193,10 +193,9 @@ test.serial('merge a project', async (t) => { t.is(initial, 'fn(() => ({ x: 1}))'); // Run the merge - await run( - `openfn merge hello-world-staging --workspace ${projectsPath} --force` + const { stdout } = await run( + `openfn merge hello-world-staging --workspace ${projectsPath} --force --log debug` ); - // Check the step is updated const merged = await readStep(); t.is(merged, "log('hello world')"); diff --git a/integration-tests/cli/test/project-v2.test.ts b/integration-tests/cli/test/project-v2.test.ts index e3ec33a8e..8beaf99f8 100644 --- a/integration-tests/cli/test/project-v2.test.ts +++ b/integration-tests/cli/test/project-v2.test.ts @@ -174,12 +174,13 @@ steps: test.serial('execute a workflow from the checked out project', async (t) => { // cheeky bonus test of checkout by alias - await run(`openfn checkout main --workspace ${TMP_DIR}`); + await run(`openfn checkout main --workspace ${TMP_DIR} --force`); // execute a workflow - await run( + const { stdout } = await run( `openfn hello-workflow -o ${TMP_DIR}/output.json --workspace ${TMP_DIR}` ); + console.log(stdout); const output = await readFile(`${TMP_DIR}/output.json`, 'utf8'); const finalState = JSON.parse(output); diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index 883feb128..f622b2a0e 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -10,12 +10,9 @@ import * as o from '../options'; import * as po from './options'; import type { Opts } from './options'; -import { - findLocallyChangedWorkflows, - tidyWorkflowDir, - updateForkedFrom, -} from './util'; +import { tidyWorkflowDir, updateForkedFrom } from './util'; import { createProjectCredentials } from './create-credentials'; +import abort from '../util/abort'; export type CheckoutOptions = Pick< Opts, @@ -52,7 +49,11 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { // TODO: try to retain the endpoint for the projects const { project: _, ...config } = workspace.getConfig() as any; - const currentProject = await workspace.getCheckedOutProject(); + const localProject = await workspace.getCheckedOutProject( + // TODO not sold on this assignment - I think my test case must be wrong + workspace.activeProject?.alias as any + ); + // get the project let switchProject; if (/\.(yaml|json)$/.test(projectIdentifier)) { @@ -71,34 +72,33 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { `Project with id ${projectIdentifier} not found in the workspace` ); } + logger?.info(`Checking out ${switchProject.alias}`); // get the current state of the checked out project try { - const localProject = await Project.from('fs', { - root: options.workspace || '.', - }); - logger?.success(`Loaded local project ${localProject.alias}`); - const changed = await findLocallyChangedWorkflows( - workspace, - localProject, - 'assume-ok' - ); - if (changed.length && !options.force) { - logger?.break(); - logger?.warn( - 'WARNING: detected changes on your currently checked-out project' - ); - logger?.warn( - `Changes may be lost by checking out ${localProject.alias} right now` + // If there's no project checked out, there's nothing to compare + if (localProject?.workflows.length) { + logger?.info( + `Loaded currently checked out project ${localProject.alias} to check for untracked changes` ); - logger?.warn(`Pass --force or -f to override this warning and continue`); - // TODO log to run with force - // TODO need to implement a save function - const e = new Error( - `The currently checked out project has diverged! Changes may be lost` - ); - delete e.stack; - throw e; + // TODO is alias robust here? Should we get by alias and domain? + const tracked = workspace.get(localProject.alias ?? localProject.id); + const changed = hasUntrackedChanges(localProject, tracked); + logger?.debug(changed); + if (changed.length && !options.force) { + const err = { + details: `Changes may be lost by checking out ${ + localProject.alias ?? localProject.id + } right now`, + // TODO how can users save changes? Not really possible right now + fix: 'Pass --force or -f to override this warning and continue', + }; + abort( + logger!, + `${switchProject.alias} has diverged from ${localProject.alias}!`, + err + ); + } } } catch (e: any) { if (e.message.match('ENOENT')) { @@ -113,7 +113,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { if (options.clean) { await rimraf(workspace.workflowsPath); } else { - await tidyWorkflowDir(currentProject, switchProject, false, workspacePath); + await tidyWorkflowDir(localProject, switchProject, false, workspacePath); } // write the forked from map @@ -137,3 +137,47 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { logger?.success(`Expanded project to ${workspacePath}`); }; + +// This function will tell us if the active/checked out project +// has any changes compared to the tracked state file +// It implies that changes will be lost on checkout +// (later, users can save a project to an arbitrary save file and so this may not be true) +const hasUntrackedChanges = ( + activeProject: Project, + tracked?: Project | null +) => { + if (!tracked) { + // if there's no tracking we can't compare + // should we log a warning then? + return []; + } + + const changedWorkflows: Array<{ + id: string; + type: 'new' | 'changed' | 'removed'; + }> = []; + + // Check for changed and added workflows + for (const workflow of activeProject.workflows) { + const trackedWorkflow = tracked.getWorkflow(workflow.id); + if (!trackedWorkflow) { + // this is a new workflow added locally + changedWorkflows.push({ id: workflow.id, type: 'new' }); + continue; + } + + if (!tracked.canMergeInto(activeProject)) { + changedWorkflows.push({ id: workflow.id, type: 'changed' }); + } + } + + // Check for removed workflows + for (const workflow of tracked.workflows) { + const localWorkflow = activeProject.getWorkflow(workflow.id); + if (!localWorkflow) { + changedWorkflows.push({ id: workflow.id, type: 'removed' }); + } + } + + return changedWorkflows; +}; diff --git a/packages/cli/src/projects/list.ts b/packages/cli/src/projects/list.ts index 06d091c59..932ad182e 100644 --- a/packages/cli/src/projects/list.ts +++ b/packages/cli/src/projects/list.ts @@ -7,6 +7,7 @@ import * as o from '../options'; import * as po from './options'; import type { Opts } from './options'; +import abort from '../util/abort'; export type ProjectListOptions = Pick; @@ -34,7 +35,9 @@ export const handler = async (options: ProjectListOptions, logger: Logger) => { // eg, this will happen if there's no openfn.yaml file // basically we need the workspace to return a reason // (again, I'm thinking of removing the validation entirely) - throw new Error('No OpenFn projects found'); + abort(logger, `No OpenFn projects found at ${options.workspace}`, { + fix: 'Run this command from a folder with an openfn.yaml file, or pass --workspace to set the workspace root', + }); } logger.always(`Available openfn projects\n\n${workspace diff --git a/packages/cli/src/projects/merge.ts b/packages/cli/src/projects/merge.ts index 0f6b1fa7e..7e797a9f3 100644 --- a/packages/cli/src/projects/merge.ts +++ b/packages/cli/src/projects/merge.ts @@ -171,6 +171,8 @@ export const handler = async (options: MergeOptions, logger: Logger) => { workspace: workspacePath, project: options.outputPath ? finalPath : final.id, log: options.log, + // after the merge, we have to force the output to be checked out, ignoring divergence + force: true, }, logger ); diff --git a/packages/cli/src/projects/util.ts b/packages/cli/src/projects/util.ts index abe1f7e7b..7f446fd22 100644 --- a/packages/cli/src/projects/util.ts +++ b/packages/cli/src/projects/util.ts @@ -259,6 +259,8 @@ export const updateForkedFrom = (proj: Project) => { return proj; }; +// Compare a project to its version hashed when forked +// This tells us whether the project was edited since it was created export const findLocallyChangedWorkflows = async ( workspace: Workspace, project: Project, @@ -266,7 +268,6 @@ export const findLocallyChangedWorkflows = async ( ) => { // Check openfn.yaml for the forked_from versions const { forked_from } = workspace.activeProject ?? {}; - // If there are no forked_from references, we have no baseline // so assume everything has changed if (!forked_from || Object.keys(forked_from).length === 0) { diff --git a/packages/cli/src/util/abort.ts b/packages/cli/src/util/abort.ts index 83aa44950..aecc3464e 100644 --- a/packages/cli/src/util/abort.ts +++ b/packages/cli/src/util/abort.ts @@ -16,24 +16,24 @@ interface CLIFriendlyError extends Error { export default ( logger: Logger, reason: string, - error?: CLIFriendlyError, + error?: Partial, help?: string ) => { const e = new AbortError(reason); logger.break(); logger.error(reason); if (error) { - logger.error(error.message); - logger.break(); + if (error.message) { + logger.error(error.message); + logger.break(); + } if (error.details) { - logger.error('ERROR DETAILS:'); logger.error(error.details); logger.break(); } if (error.fix) { - logger.error('FIX HINT:'); - logger.error(error.fix); + logger.always(error.fix); logger.break(); } } @@ -41,7 +41,7 @@ export default ( logger.always(help); } logger.break(); - logger.error('Critical error: aborting command'); + // logger.error('Critical error: aborting command'); process.exitCode = 1; diff --git a/packages/cli/test/compile/compile.test.ts b/packages/cli/test/compile/compile.test.ts index 1265fb44a..6acdb9bf8 100644 --- a/packages/cli/test/compile/compile.test.ts +++ b/packages/cli/test/compile/compile.test.ts @@ -89,7 +89,6 @@ test.serial('throw an AbortError if a job is uncompilable', async (t) => { t.assert(logger._find('error', /unexpected token/i)); t.assert(logger._find('always', /check the syntax of the job expression/i)); - t.assert(logger._find('error', /critical error: aborting command/i)); }); test.serial( @@ -111,7 +110,6 @@ test.serial( t.assert(logger._find('error', /unexpected token/i)); t.assert(logger._find('always', /check the syntax of the job expression/i)); - t.assert(logger._find('error', /critical error: aborting command/i)); } ); diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index 3a110c5bf..628bcad8c 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -530,6 +530,8 @@ test.serial( command: 'project-checkout', project: 'main-project', workspace: '/ws3', + // the project on-disk has diverged from the statefile, so we need to force it through + force: true, }, logger ); @@ -570,6 +572,8 @@ test.serial( command: 'project-checkout', project: 'main-project', workspace: '/ws4', + // the project on-disk has diverged from the statefile, so we need to force it through + force: true, }, logger ); @@ -714,6 +718,8 @@ test.serial( command: 'project-checkout', project: 'main-project', workspace: '/ws5', + // the project on-disk has diverged from the statefile, so we need to force it through + force: true, }, logger ); @@ -722,3 +728,269 @@ test.serial( t.true(fs.existsSync('/ws5/workflows/workflow-a')); } ); + +/** + * Using projects foo and bar here which come from a real issue + * Keeping those exact state files to keep diversity in the tests + */ +const foo = `id: foo +name: foo +schema_version: '4.0' +collections: [] +channels: [] +credentials: + - uuid: 8c675997-117b-4e8a-a65e-1ddea0d0e525 + name: name + owner: editor@openfn.org +openfn: + uuid: 44c0c920-5635-4984-ade2-b95fb24cbaf0 + endpoint: http://localhost:4000 + inserted_at: 2025-10-15T11:29:36Z + updated_at: 2026-03-17T11:59:53Z +options: + env: main + allow_support_access: false + requires_mfa: false + retention_policy: retain_all +workflows: + - name: A + steps: + - id: aaa + name: aaa + expression: // abc + adaptor: '@openfn/language-common@latest' + openfn: + uuid: 7b6a6de4-eed2-4204-8ac0-4da8fa64206c + next: + bbb: + disabled: false + condition: on_job_success + openfn: + uuid: 64f1b20f-bfdf-4626-87de-403008cfb05d + - id: bbb + name: bbb + expression: '2' + adaptor: '@openfn/language-common@3.3.1' + openfn: + uuid: 832f5560-69c5-4eae-89cc-823b93af82c8 + - id: webhook + type: webhook + enabled: true + webhook_reply: before_start + openfn: + uuid: 16ddedbb-1d70-44b7-8653-26f8dc802757 + next: + aaa: + disabled: false + condition: always + openfn: + uuid: eccb03ef-990d-4ca7-877b-5452bbc8f63b + history: + - app:0a97362c97b3 + - app:8eb248f07744 + openfn: + uuid: 4b2c13aa-2497-421a-9bb2-783309254130 + updated_at: 2026-05-14T10:25:36Z + inserted_at: 2026-05-14T10:25:10Z + lock_version: 6 + id: a + start: webhook +`; +const bar = `id: bar +name: bar +schema_version: '4.0' +cli: + forked_from: + a: cli:145ff1ae62e5 +collections: [] +channels: [] +credentials: [] +openfn: + uuid: 7c478de6-4c82-427d-aad2-875b1b9eccb8 + endpoint: http://localhost:4000 + alias: staging + inserted_at: 2026-05-26T16:27:05Z + updated_at: 2026-05-26T16:27:05Z +options: + allow_support_access: false + requires_mfa: false + retention_policy: retain_all +workflows: + - name: A + steps: + - id: aaa + name: aaa + expression: // 2 + adaptor: '@openfn/language-common@latest' + openfn: + uuid: 8227ae53-81f8-447f-bb93-213d5721f884 + next: + bbb: + disabled: false + condition: on_job_success + openfn: + uuid: 474d6861-bb47-4fad-953d-a7762751bae0 + - id: bbb + name: bbb + expression: '2' + adaptor: '@openfn/language-http@7.2.11' + openfn: + uuid: 862bec16-ef94-4438-b307-8594a70276fe + - id: webhook + type: webhook + enabled: false + webhook_reply: before_start + openfn: + uuid: d7dfdd68-ecb8-4adc-90cf-8a4ed8cc0235 + next: + aaa: + disabled: false + condition: always + openfn: + uuid: 067cab97-bef8-4d70-b484-5d013d27142b + history: + - cli:145ff1ae62e5 + openfn: + uuid: 9746c1d9-1499-4413-9edc-c23577e9308e + inserted_at: 2026-05-26T16:27:05Z + updated_at: 2026-05-26T16:27:05Z + lock_version: 1 + id: a + start: webhook +`; + +test.serial( + 'Checkout unrelated bar from unrelated project foo without divergence warning', + async (t) => { + mock({ + '/tmp/openfn.yaml': '', + '/tmp/.projects/main@server.yaml': foo, + '/tmp/.projects/staging@server.yaml': bar, + }); + + // first checkout foo to set up the file system + await checkoutHandler( + { + command: 'project-checkout', + project: 'foo', + workspace: '/tmp', + }, + logger + ); + + // assert that staging was checked out ok + let openfn = yamlToJson(fs.readFileSync('/tmp/openfn.yaml', 'utf8')); + t.is(openfn.project.id, 'foo'); + + let expression = fs.readFileSync('/tmp/workflows/a/aaa.js', 'utf8'); + t.is(expression, '// abc'); + + // now checkout bar + await checkoutHandler( + { + command: 'project-checkout', + project: 'bar', + workspace: '/tmp', + }, + logger + ); + logger._reset(); + + // assert that main was checked out ok + openfn = yamlToJson(fs.readFileSync('/tmp/openfn.yaml', 'utf8')); + t.is(openfn.project.id, 'bar'); + + expression = fs.readFileSync('/tmp/workflows/a/aaa.js', 'utf8'); + t.is(expression, '// 2'); + } +); + +test.serial( + 'Checkout unrelated foo from unrelated project bar without divergence warning', + async (t) => { + mock({ + '/tmp/openfn.yaml': '', + '/tmp/.projects/main@server.yaml': foo, + '/tmp/.projects/staging@server.yaml': bar, + }); + + // first checkout bar to set up the file system + await checkoutHandler( + { + command: 'project-checkout', + project: 'bar', + workspace: '/tmp', + }, + logger + ); + logger._reset(); + + // assert that main was checked out ok + let openfn = yamlToJson(fs.readFileSync('/tmp/openfn.yaml', 'utf8')); + t.is(openfn.project.id, 'bar'); + + let expression = fs.readFileSync('/tmp/workflows/a/aaa.js', 'utf8'); + t.is(expression, '// 2'); + + // now checkout foo + await checkoutHandler( + { + command: 'project-checkout', + project: 'foo', + workspace: '/tmp', + }, + logger + ); + + // assert that staging was checked out ok + openfn = yamlToJson(fs.readFileSync('/tmp/openfn.yaml', 'utf8')); + t.is(openfn.project.id, 'foo'); + + expression = fs.readFileSync('/tmp/workflows/a/aaa.js', 'utf8'); + t.is(expression, '// abc'); + } +); + +test.serial( + 'If the checked out project has diverged from the tracked version, show a divergence warning on checkout', + async (t) => { + mock({ + '/tmp/openfn.yaml': '', + '/tmp/.projects/main@server.yaml': foo, + '/tmp/.projects/staging@server.yaml': bar, + }); + + await checkoutHandler( + { + command: 'project-checkout', + project: 'bar', + workspace: '/tmp', + }, + logger + ); + logger._reset(); + + // assert that main was checked out ok + let openfn = yamlToJson(fs.readFileSync('/tmp/openfn.yaml', 'utf8')); + t.is(openfn.project.id, 'bar'); + + // Now make a change - on checkout, this change will be lost (it is not saved anywhere) + fs.writeFileSync('/tmp/workflows/a/aaa.js', 'foobar'); + + // now try to checkout foo + await t.throwsAsync( + () => + checkoutHandler( + { + command: 'project-checkout', + project: 'foo', + workspace: '/tmp', + }, + logger + ), + { + message: 'main has diverged from staging!', + } + ); + } +); diff --git a/packages/cli/test/projects/list.test.ts b/packages/cli/test/projects/list.test.ts index 6b4b2d05e..0891e5038 100644 --- a/packages/cli/test/projects/list.test.ts +++ b/packages/cli/test/projects/list.test.ts @@ -135,7 +135,7 @@ test('throw for invalid workspace directory', async (t) => { await t.throwsAsync( () => list({ command: 'projects', workspace: '/invalid' }, logger), { - message: 'No OpenFn projects found', + message: 'No OpenFn projects found at /invalid', } ); // const { message } = logger._parse(logger._last); @@ -146,7 +146,7 @@ test('throw if dir is not a workspace', async (t) => { await t.throwsAsync( () => list({ command: 'projects', workspace: '/no-ws' }, logger), { - message: 'No OpenFn projects found', + message: 'No OpenFn projects found at /no-ws', } ); }); diff --git a/packages/lexicon/core.d.ts b/packages/lexicon/core.d.ts index bdf6be58c..1158ffb69 100644 --- a/packages/lexicon/core.d.ts +++ b/packages/lexicon/core.d.ts @@ -94,7 +94,7 @@ export interface LocalMeta { This only affects how a state file ondisk is parsed */ version?: number; /** Shorthand identifier used by CLI commands */ - alias?: string; + alias?: string | null; [key: string]: any; } diff --git a/packages/project/src/Project.ts b/packages/project/src/Project.ts index 749600b26..c1a401db4 100644 --- a/packages/project/src/Project.ts +++ b/packages/project/src/Project.ts @@ -36,7 +36,7 @@ type UUIDMap = { type CLIMeta = { version?: number; - alias?: string; + alias?: string | null; forked_from?: Record; }; @@ -169,11 +169,11 @@ export class Project { } /** Local alias for the project. Comes from the file name. Not shared with Lightning. */ - get alias() { - return this.cli.alias ?? 'main'; + get alias(): string | null { + return this.cli.alias ?? null; } - set alias(value: string) { + set alias(value: string | null) { this.cli ??= {}; this.cli.alias = value; } diff --git a/packages/project/src/Workflow.ts b/packages/project/src/Workflow.ts index 8fcce5868..b4d42d237 100644 --- a/packages/project/src/Workflow.ts +++ b/packages/project/src/Workflow.ts @@ -208,7 +208,6 @@ class Workflow { this.workflow.history?.concat(this.getVersionHash()) ?? []; const targetHistory = target.workflow.history?.concat(target.getVersionHash()) ?? []; - const targetHead = targetHistory[targetHistory.length - 1]; return thisHistory.indexOf(targetHead) > -1; } diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index 0f7eb847b..f7e2bc3ec 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -48,6 +48,13 @@ export class Workspace { } } this.config = buildConfig(context.workspace); + + // TODO: work out the alias of the active project + // and make sure it's written + // tbh as activeProject is just the metadata in openfn.yaml, + // it's not super reliable + // Actually would it not be better to find the ACTUAL project and just + // reference that? this.activeProject = context.project; const projectsPath = path.join(workspacePath, this.config.dirs.projects); @@ -123,10 +130,13 @@ export class Workspace { ); } - async getCheckedOutProject() { + async getCheckedOutProject(alias?: string | null) { return await Project.from('fs', { root: this.root, config: this.config, + // The checked out project can't meaningfully be said to have an alias + // But we can force one if it makes sense from context + alias: alias ?? null, }).catch((e) => { if (e.code === 'ENOENT') return undefined; throw e; diff --git a/packages/project/src/parse/from-fs.ts b/packages/project/src/parse/from-fs.ts index 1382eecba..dd6bad57a 100644 --- a/packages/project/src/parse/from-fs.ts +++ b/packages/project/src/parse/from-fs.ts @@ -19,7 +19,7 @@ export type FromFsConfig = { root: string; config?: Partial; logger?: Logger; - alias?: string; + alias?: string | null; name?: string; };