2026-05-10 23:12:26 +02:00
|
|
|
import { existsSync } from 'node:fs';
|
|
|
|
|
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
|
|
|
import { homedir } from 'node:os';
|
2026-05-14 17:39:31 +02:00
|
|
|
import { join, resolve } from 'node:path';
|
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
|
|
|
import { initKtxProject, type KtxLocalProject, loadKtxProject } from './context/project/project.js';
|
|
|
|
|
import { markKtxSetupStateStepComplete, mergeKtxSetupGitignoreEntries } from './context/project/setup-config.js';
|
|
|
|
|
import { serializeKtxProjectConfig } from './context/project/config.js';
|
2026-05-10 23:51:24 +02:00
|
|
|
import type { KtxCliIo } from './cli-runtime.js';
|
2026-05-13 14:27:29 +02:00
|
|
|
import { gray } from './io/symbols.js';
|
2026-05-13 17:01:48 +02:00
|
|
|
import { withTextInputNavigation } from './prompt-navigation.js';
|
|
|
|
|
import {
|
|
|
|
|
createKtxSetupPromptAdapter,
|
|
|
|
|
type KtxSetupPromptOption,
|
|
|
|
|
} from './setup-prompts.js';
|
2026-05-10 23:12:26 +02:00
|
|
|
|
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
|
|
|
type KtxSetupProjectMode = 'auto' | 'prompt-new';
|
|
|
|
|
type KtxSetupInputMode = 'auto' | 'disabled';
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export interface KtxSetupProjectArgs {
|
2026-05-10 23:12:26 +02:00
|
|
|
projectDir: string;
|
2026-05-10 23:51:24 +02:00
|
|
|
mode: KtxSetupProjectMode;
|
|
|
|
|
inputMode: KtxSetupInputMode;
|
2026-05-10 23:12:26 +02:00
|
|
|
yes: boolean;
|
|
|
|
|
allowBack?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export type KtxSetupProjectResult =
|
2026-05-19 18:18:38 +02:00
|
|
|
| {
|
|
|
|
|
status: 'ready';
|
|
|
|
|
projectDir: string;
|
|
|
|
|
project: KtxLocalProject;
|
|
|
|
|
confirmedCreation?: boolean;
|
|
|
|
|
}
|
2026-05-10 23:12:26 +02:00
|
|
|
| { status: 'back'; projectDir: string }
|
|
|
|
|
| { status: 'cancelled'; projectDir: string }
|
|
|
|
|
| { status: 'missing-input'; projectDir: string };
|
|
|
|
|
|
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
|
|
|
/** @internal */
|
2026-05-10 23:51:24 +02:00
|
|
|
export interface KtxSetupProjectPromptAdapter {
|
2026-05-13 17:01:48 +02:00
|
|
|
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
2026-05-10 23:12:26 +02:00
|
|
|
text(options: { message: string; placeholder?: string }): Promise<string | undefined>;
|
|
|
|
|
cancel(message: string): void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export interface KtxSetupProjectDeps {
|
|
|
|
|
prompts?: KtxSetupProjectPromptAdapter;
|
|
|
|
|
initProject?: typeof initKtxProject;
|
|
|
|
|
loadProject?: typeof loadKtxProject;
|
2026-05-10 23:12:26 +02:00
|
|
|
homeDir?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type PromptProjectDirResult =
|
2026-05-19 18:18:38 +02:00
|
|
|
| {
|
|
|
|
|
status: 'selected';
|
|
|
|
|
projectDir: string;
|
|
|
|
|
confirmedCreation: boolean;
|
|
|
|
|
}
|
2026-05-10 23:12:26 +02:00
|
|
|
| { status: 'cancelled'; projectDir: string }
|
|
|
|
|
| { status: 'missing-input'; projectDir: string }
|
|
|
|
|
| { status: 'back'; projectDir: string };
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
const DEFAULT_NEW_PROJECT_FOLDER_NAME = 'ktx-project';
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function createClackSetupProjectPromptAdapter(): KtxSetupProjectPromptAdapter {
|
2026-05-13 17:01:48 +02:00
|
|
|
return createKtxSetupPromptAdapter({ selectCancelValue: 'exit' });
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasProjectConfig(projectDir: string): boolean {
|
2026-05-10 23:51:24 +02:00
|
|
|
return existsSync(join(projectDir, 'ktx.yaml'));
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveFromProjectDir(projectDir: string, input: string, homeDir: string): string {
|
|
|
|
|
if (input === '~') {
|
|
|
|
|
return resolve(homeDir);
|
|
|
|
|
}
|
|
|
|
|
if (input.startsWith('~/') || input.startsWith('~\\')) {
|
|
|
|
|
return resolve(homeDir, input.slice(2));
|
|
|
|
|
}
|
|
|
|
|
return resolve(projectDir, input);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function existingFolderState(
|
|
|
|
|
projectDir: string,
|
|
|
|
|
): Promise<'missing' | 'empty-directory' | 'non-empty-directory' | 'not-directory'> {
|
|
|
|
|
try {
|
|
|
|
|
const projectDirStat = await stat(projectDir);
|
|
|
|
|
if (!projectDirStat.isDirectory()) {
|
|
|
|
|
return 'not-directory';
|
|
|
|
|
}
|
|
|
|
|
return (await readdir(projectDir)).length === 0 ? 'empty-directory' : 'non-empty-directory';
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
|
|
|
return 'missing';
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 16:59:30 -07:00
|
|
|
type ConfirmProjectDirResult =
|
2026-05-19 18:18:38 +02:00
|
|
|
| {
|
|
|
|
|
status: 'confirmed';
|
|
|
|
|
confirmedCreation: boolean;
|
|
|
|
|
}
|
2026-05-12 16:59:30 -07:00
|
|
|
| { status: 'choose-another' }
|
|
|
|
|
| { status: 'back' }
|
|
|
|
|
| { status: 'cancelled' }
|
|
|
|
|
| { status: 'not-directory' };
|
|
|
|
|
|
|
|
|
|
async function confirmProjectDir(
|
|
|
|
|
selectedDir: string,
|
|
|
|
|
io: KtxCliIo,
|
|
|
|
|
prompts: KtxSetupProjectPromptAdapter,
|
|
|
|
|
): Promise<ConfirmProjectDirResult> {
|
|
|
|
|
const state = await existingFolderState(selectedDir);
|
|
|
|
|
|
|
|
|
|
if (state === 'not-directory') {
|
|
|
|
|
io.stderr.write(`Project folder path exists and is not a directory: ${selectedDir}\n`);
|
|
|
|
|
return { status: 'not-directory' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state === 'non-empty-directory') {
|
|
|
|
|
const action = await prompts.select({
|
|
|
|
|
message: `That folder already exists and is not empty: ${selectedDir}`,
|
|
|
|
|
options: [
|
|
|
|
|
{ value: 'use-existing', label: 'Yes, create KTX files there' },
|
|
|
|
|
{ value: 'choose-another', label: 'Choose another folder' },
|
|
|
|
|
{ value: 'back', label: 'Back' },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
if (action === 'choose-another') return { status: 'choose-another' };
|
|
|
|
|
if (action === 'back') return { status: 'back' };
|
|
|
|
|
if (action !== 'use-existing') return { status: 'cancelled' };
|
|
|
|
|
return { status: 'confirmed', confirmedCreation: true };
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 17:03:03 -07:00
|
|
|
io.stdout.write(`│ KTX will create:\n│ ${selectedDir}\n`);
|
2026-05-12 16:59:30 -07:00
|
|
|
const action = await prompts.select({
|
|
|
|
|
message: `Create KTX project at ${selectedDir}?`,
|
|
|
|
|
options: [
|
|
|
|
|
{ value: 'create', label: 'Create project' },
|
|
|
|
|
{ value: 'choose-another', label: 'Choose another folder' },
|
|
|
|
|
{ value: 'back', label: 'Back' },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
if (action === 'choose-another') return { status: 'choose-another' };
|
|
|
|
|
if (action === 'back') return { status: 'back' };
|
|
|
|
|
if (action !== 'create') return { status: 'cancelled' };
|
fix(cli): preserve project artifacts when ktx setup steps fail (#229)
ktx setup wiped ktx.yaml, .ktx/setup/state.json, wiki/, semantic-layer/,
raw-sources/, and .git/ — or removed the entire project dir — whenever any
single source in the context-build step failed, destroying hours of ingest
work and the persisted resume state. The cleanup hint was designed for an
"early abort, leave no trace" semantic but was applied indiscriminately to
every later step failure, in direct conflict with the .ktx/setup/state.json
resume mechanism.
Drop the cleanup mechanism entirely (KtxSetupCreatedProjectCleanup,
cleanupForFolderState, createProjectWithCleanup, cleanupCreatedProjectScaffold,
and the createdProjectCleanup plumbing through KtxSetupProjectResult). Step
failures now return non-zero without touching the filesystem, so re-running
ktx setup continues from completed steps and only re-attempts failed sources.
Rewrites the two tests that documented the wipe behavior to assert
preservation, and adds a regression test that simulates partial context-build
artifacts (state.json, wiki/, semantic-layer/) and verifies all survive a
failed context step.
Refs KLO-719
2026-05-28 15:17:06 +02:00
|
|
|
return { status: 'confirmed', confirmedCreation: true };
|
2026-05-12 16:59:30 -07:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
async function normalizeSetupGitignore(projectDir: string): Promise<void> {
|
2026-05-10 23:51:24 +02:00
|
|
|
const gitignorePath = join(projectDir, '.ktx/.gitignore');
|
|
|
|
|
await mkdir(join(projectDir, '.ktx'), { recursive: true });
|
2026-05-10 23:12:26 +02:00
|
|
|
const current = existsSync(gitignorePath) ? await readFile(gitignorePath, 'utf-8') : '';
|
2026-05-10 23:51:24 +02:00
|
|
|
await writeFile(gitignorePath, mergeKtxSetupGitignoreEntries(current), 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
async function persistProjectStep(project: KtxLocalProject): Promise<KtxLocalProject> {
|
2026-05-13 13:55:21 +02:00
|
|
|
await writeFile(project.configPath, serializeKtxProjectConfig(project.config), 'utf-8');
|
2026-05-12 16:26:23 -07:00
|
|
|
await markKtxSetupStateStepComplete(project.projectDir, 'project');
|
2026-05-10 23:12:26 +02:00
|
|
|
await normalizeSetupGitignore(project.projectDir);
|
2026-05-10 23:51:24 +02:00
|
|
|
return await loadKtxProject({ projectDir: project.projectDir });
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
async function createProject(projectDir: string, deps: KtxSetupProjectDeps): Promise<KtxLocalProject> {
|
|
|
|
|
const initProject = deps.initProject ?? initKtxProject;
|
2026-05-14 17:39:31 +02:00
|
|
|
const initialized = await initProject({ projectDir });
|
2026-05-10 23:12:26 +02:00
|
|
|
return await persistProjectStep(initialized);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
async function loadExistingProject(projectDir: string, deps: KtxSetupProjectDeps): Promise<KtxLocalProject> {
|
|
|
|
|
const loadProject = deps.loadProject ?? loadKtxProject;
|
2026-05-10 23:12:26 +02:00
|
|
|
const project = await loadProject({ projectDir });
|
|
|
|
|
return await persistProjectStep(project);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
function printProjectSummary(io: KtxCliIo, projectDir: string): void {
|
2026-05-12 16:58:09 -07:00
|
|
|
io.stdout.write(`│ Project: ${projectDir}\n`);
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function promptForNewProjectDir(
|
|
|
|
|
projectDir: string,
|
|
|
|
|
homeDir: string,
|
2026-05-10 23:51:24 +02:00
|
|
|
io: KtxCliIo,
|
|
|
|
|
prompts: KtxSetupProjectPromptAdapter,
|
2026-05-10 23:12:26 +02:00
|
|
|
): Promise<PromptProjectDirResult> {
|
|
|
|
|
const defaultProjectDir = join(projectDir, DEFAULT_NEW_PROJECT_FOLDER_NAME);
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
const destinationChoice = await prompts.select({
|
2026-05-10 23:51:24 +02:00
|
|
|
message: 'Where should KTX create the project?',
|
2026-05-10 23:12:26 +02:00
|
|
|
options: [
|
|
|
|
|
{ value: 'default', label: `Create the default project folder: ${defaultProjectDir}` },
|
|
|
|
|
{ value: 'custom', label: 'Enter a custom path' },
|
|
|
|
|
{ value: 'back', label: 'Back' },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let selectedDir: string;
|
|
|
|
|
if (destinationChoice === 'back') {
|
|
|
|
|
return { status: 'back', projectDir };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (destinationChoice === 'default') {
|
|
|
|
|
selectedDir = defaultProjectDir;
|
|
|
|
|
} else if (destinationChoice === 'custom') {
|
|
|
|
|
const rawSelectedDir = await prompts.text({
|
|
|
|
|
message: withTextInputNavigation('Project folder path'),
|
2026-05-10 23:51:24 +02:00
|
|
|
placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx',
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
if (rawSelectedDir === undefined) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const trimmedSelectedDir = rawSelectedDir.trim();
|
|
|
|
|
if (trimmedSelectedDir.length === 0) {
|
|
|
|
|
io.stderr.write(
|
2026-05-10 23:51:24 +02:00
|
|
|
'Enter a relative path like ./analytics-ktx, a home path like ~/analytics-ktx, or an absolute path.\n',
|
2026-05-10 23:12:26 +02:00
|
|
|
);
|
|
|
|
|
return { status: 'missing-input', projectDir };
|
|
|
|
|
}
|
|
|
|
|
selectedDir = resolveFromProjectDir(projectDir, trimmedSelectedDir, homeDir);
|
|
|
|
|
} else {
|
|
|
|
|
return { status: 'cancelled', projectDir };
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 16:59:30 -07:00
|
|
|
const confirmed = await confirmProjectDir(selectedDir, io, prompts);
|
|
|
|
|
if (confirmed.status === 'not-directory') return { status: 'missing-input', projectDir };
|
|
|
|
|
if (confirmed.status === 'choose-another') continue;
|
|
|
|
|
if (confirmed.status === 'back') return { status: 'back', projectDir };
|
|
|
|
|
if (confirmed.status === 'cancelled') return { status: 'cancelled', projectDir };
|
2026-05-19 18:18:38 +02:00
|
|
|
return {
|
|
|
|
|
status: 'selected',
|
|
|
|
|
projectDir: selectedDir,
|
|
|
|
|
confirmedCreation: confirmed.confirmedCreation,
|
|
|
|
|
};
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
export async function runKtxSetupProjectStep(
|
|
|
|
|
args: KtxSetupProjectArgs,
|
|
|
|
|
io: KtxCliIo,
|
|
|
|
|
deps: KtxSetupProjectDeps = {},
|
|
|
|
|
): Promise<KtxSetupProjectResult> {
|
2026-05-10 23:12:26 +02:00
|
|
|
const projectDir = resolve(args.projectDir);
|
|
|
|
|
const homeDir = deps.homeDir ?? homedir();
|
|
|
|
|
const exists = hasProjectConfig(projectDir);
|
|
|
|
|
|
|
|
|
|
if (args.mode === 'prompt-new') {
|
|
|
|
|
if (args.inputMode === 'disabled') {
|
2026-05-19 19:23:35 +02:00
|
|
|
io.stderr.write('Missing new project folder: pass --project-dir and --yes to create a project without prompts.\n');
|
2026-05-10 23:12:26 +02:00
|
|
|
return { status: 'missing-input', projectDir };
|
|
|
|
|
}
|
|
|
|
|
if (!io.stdout.isTTY && !deps.prompts) {
|
|
|
|
|
io.stderr.write(
|
2026-05-19 19:23:35 +02:00
|
|
|
'Missing new project folder: pass --project-dir and --yes to create a project outside an interactive terminal.\n',
|
2026-05-10 23:12:26 +02:00
|
|
|
);
|
|
|
|
|
return { status: 'missing-input', projectDir };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const prompts = deps.prompts ?? createClackSetupProjectPromptAdapter();
|
|
|
|
|
const selected = await promptForNewProjectDir(projectDir, homeDir, io, prompts);
|
|
|
|
|
if (selected.status === 'back') {
|
|
|
|
|
return args.allowBack ? { status: 'back', projectDir } : { status: 'cancelled', projectDir };
|
|
|
|
|
}
|
|
|
|
|
if (selected.status !== 'selected') {
|
|
|
|
|
return selected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const project = await createProject(selected.projectDir, deps);
|
|
|
|
|
printProjectSummary(io, selected.projectDir);
|
|
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir: selected.projectDir,
|
|
|
|
|
project,
|
|
|
|
|
confirmedCreation: selected.confirmedCreation,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (exists) {
|
|
|
|
|
const project = await loadExistingProject(projectDir, deps);
|
|
|
|
|
printProjectSummary(io, projectDir);
|
|
|
|
|
return { status: 'ready', projectDir, project };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (args.inputMode === 'disabled') {
|
|
|
|
|
if (!args.yes) {
|
2026-05-19 19:23:35 +02:00
|
|
|
io.stderr.write('Missing setup choice: pass --yes to create a project in non-interactive setup.\n');
|
2026-05-10 23:12:26 +02:00
|
|
|
return { status: 'missing-input', projectDir };
|
|
|
|
|
}
|
fix(cli): preserve project artifacts when ktx setup steps fail (#229)
ktx setup wiped ktx.yaml, .ktx/setup/state.json, wiki/, semantic-layer/,
raw-sources/, and .git/ — or removed the entire project dir — whenever any
single source in the context-build step failed, destroying hours of ingest
work and the persisted resume state. The cleanup hint was designed for an
"early abort, leave no trace" semantic but was applied indiscriminately to
every later step failure, in direct conflict with the .ktx/setup/state.json
resume mechanism.
Drop the cleanup mechanism entirely (KtxSetupCreatedProjectCleanup,
cleanupForFolderState, createProjectWithCleanup, cleanupCreatedProjectScaffold,
and the createdProjectCleanup plumbing through KtxSetupProjectResult). Step
failures now return non-zero without touching the filesystem, so re-running
ktx setup continues from completed steps and only re-attempts failed sources.
Rewrites the two tests that documented the wipe behavior to assert
preservation, and adds a regression test that simulates partial context-build
artifacts (state.json, wiki/, semantic-layer/) and verifies all survive a
failed context step.
Refs KLO-719
2026-05-28 15:17:06 +02:00
|
|
|
const project = await createProject(projectDir, deps);
|
2026-05-10 23:12:26 +02:00
|
|
|
printProjectSummary(io, projectDir);
|
2026-05-19 18:18:38 +02:00
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir,
|
|
|
|
|
project,
|
|
|
|
|
};
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!io.stdout.isTTY && !deps.prompts) {
|
2026-05-19 19:23:35 +02:00
|
|
|
io.stderr.write('Missing setup choice: pass --yes to create a project outside an interactive terminal.\n');
|
2026-05-10 23:12:26 +02:00
|
|
|
return { status: 'missing-input', projectDir };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const prompts = deps.prompts ?? createClackSetupProjectPromptAdapter();
|
2026-05-12 16:59:30 -07:00
|
|
|
const defaultProjectDir = join(projectDir, DEFAULT_NEW_PROJECT_FOLDER_NAME);
|
2026-05-13 14:27:29 +02:00
|
|
|
const defaultProjectDirLabel = [
|
|
|
|
|
gray(defaultProjectDir.slice(0, -DEFAULT_NEW_PROJECT_FOLDER_NAME.length)),
|
|
|
|
|
DEFAULT_NEW_PROJECT_FOLDER_NAME,
|
|
|
|
|
].join('');
|
2026-05-10 23:12:26 +02:00
|
|
|
io.stdout.write(
|
2026-05-12 15:46:56 -07:00
|
|
|
'│ Use Up/Down to move, Enter to confirm the current selection, choose Back to return to the previous step, Ctrl+C to exit.\n',
|
2026-05-10 23:12:26 +02:00
|
|
|
);
|
|
|
|
|
while (true) {
|
|
|
|
|
const choice = await prompts.select({
|
2026-05-12 16:59:30 -07:00
|
|
|
message: 'Where should KTX create the project?',
|
2026-05-10 23:12:26 +02:00
|
|
|
options: [
|
2026-05-13 14:27:29 +02:00
|
|
|
{ value: 'current', label: `Current directory (${projectDir})` },
|
|
|
|
|
{ value: 'new-default', label: `New subfolder (${defaultProjectDirLabel})` },
|
2026-05-12 16:59:30 -07:00
|
|
|
{ value: 'new-custom', label: 'Custom path' },
|
2026-05-10 23:12:26 +02:00
|
|
|
...(args.allowBack ? [{ value: 'back', label: 'Back' }] : []),
|
|
|
|
|
...(args.allowBack ? [] : [{ value: 'exit', label: 'Exit' }]),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (choice === 'back') {
|
|
|
|
|
return args.allowBack ? { status: 'back', projectDir } : { status: 'cancelled', projectDir };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === 'exit') {
|
|
|
|
|
prompts.cancel('Setup cancelled.');
|
|
|
|
|
return { status: 'cancelled', projectDir };
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 16:59:30 -07:00
|
|
|
if (choice === 'current') {
|
fix(cli): preserve project artifacts when ktx setup steps fail (#229)
ktx setup wiped ktx.yaml, .ktx/setup/state.json, wiki/, semantic-layer/,
raw-sources/, and .git/ — or removed the entire project dir — whenever any
single source in the context-build step failed, destroying hours of ingest
work and the persisted resume state. The cleanup hint was designed for an
"early abort, leave no trace" semantic but was applied indiscriminately to
every later step failure, in direct conflict with the .ktx/setup/state.json
resume mechanism.
Drop the cleanup mechanism entirely (KtxSetupCreatedProjectCleanup,
cleanupForFolderState, createProjectWithCleanup, cleanupCreatedProjectScaffold,
and the createdProjectCleanup plumbing through KtxSetupProjectResult). Step
failures now return non-zero without touching the filesystem, so re-running
ktx setup continues from completed steps and only re-attempts failed sources.
Rewrites the two tests that documented the wipe behavior to assert
preservation, and adds a regression test that simulates partial context-build
artifacts (state.json, wiki/, semantic-layer/) and verifies all survive a
failed context step.
Refs KLO-719
2026-05-28 15:17:06 +02:00
|
|
|
const project = await createProject(projectDir, deps);
|
2026-05-12 16:59:30 -07:00
|
|
|
printProjectSummary(io, projectDir);
|
2026-05-19 18:18:38 +02:00
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir,
|
|
|
|
|
project,
|
|
|
|
|
};
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 16:59:30 -07:00
|
|
|
if (choice === 'new-default') {
|
|
|
|
|
const confirmed = await confirmProjectDir(defaultProjectDir, io, prompts);
|
|
|
|
|
if (confirmed.status === 'choose-another' || confirmed.status === 'back') continue;
|
|
|
|
|
if (confirmed.status === 'not-directory') return { status: 'missing-input', projectDir };
|
|
|
|
|
if (confirmed.status === 'cancelled') return { status: 'cancelled', projectDir };
|
|
|
|
|
const project = await createProject(defaultProjectDir, deps);
|
|
|
|
|
printProjectSummary(io, defaultProjectDir);
|
|
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir: defaultProjectDir,
|
|
|
|
|
project,
|
|
|
|
|
confirmedCreation: confirmed.confirmedCreation,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === 'new-custom') {
|
|
|
|
|
const rawPath = await prompts.text({
|
|
|
|
|
message: withTextInputNavigation('Project folder path'),
|
|
|
|
|
placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx',
|
|
|
|
|
});
|
|
|
|
|
if (rawPath === undefined) continue;
|
|
|
|
|
const trimmed = rawPath.trim();
|
|
|
|
|
if (trimmed.length === 0) {
|
|
|
|
|
io.stderr.write(
|
|
|
|
|
'Enter a relative path like ./analytics-ktx, a home path like ~/analytics-ktx, or an absolute path.\n',
|
|
|
|
|
);
|
|
|
|
|
return { status: 'missing-input', projectDir };
|
|
|
|
|
}
|
|
|
|
|
const customDir = resolveFromProjectDir(projectDir, trimmed, homeDir);
|
|
|
|
|
const confirmed = await confirmProjectDir(customDir, io, prompts);
|
|
|
|
|
if (confirmed.status === 'choose-another' || confirmed.status === 'back') continue;
|
|
|
|
|
if (confirmed.status === 'not-directory') return { status: 'missing-input', projectDir };
|
|
|
|
|
if (confirmed.status === 'cancelled') return { status: 'cancelled', projectDir };
|
|
|
|
|
const project = await createProject(customDir, deps);
|
|
|
|
|
printProjectSummary(io, customDir);
|
2026-05-19 18:18:38 +02:00
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir: customDir,
|
|
|
|
|
project,
|
|
|
|
|
confirmedCreation: confirmed.confirmedCreation,
|
|
|
|
|
};
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 16:59:30 -07:00
|
|
|
prompts.cancel('Setup cancelled.');
|
|
|
|
|
return { status: 'cancelled', projectDir };
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
}
|