@nlaprell/shipit 1.0.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.
Files changed (160) hide show
  1. package/.cursor/commands/create_intent_from_issue.md +28 -0
  2. package/.cursor/commands/create_pr.md +28 -0
  3. package/.cursor/commands/dashboard.md +39 -0
  4. package/.cursor/commands/deploy.md +152 -0
  5. package/.cursor/commands/drift_check.md +36 -0
  6. package/.cursor/commands/fix.md +39 -0
  7. package/.cursor/commands/generate_release_plan.md +31 -0
  8. package/.cursor/commands/generate_roadmap.md +38 -0
  9. package/.cursor/commands/help.md +37 -0
  10. package/.cursor/commands/init_project.md +26 -0
  11. package/.cursor/commands/kill.md +72 -0
  12. package/.cursor/commands/new_intent.md +68 -0
  13. package/.cursor/commands/pr.md +77 -0
  14. package/.cursor/commands/revert-plan.md +58 -0
  15. package/.cursor/commands/risk.md +64 -0
  16. package/.cursor/commands/rollback.md +43 -0
  17. package/.cursor/commands/scope_project.md +53 -0
  18. package/.cursor/commands/ship.md +345 -0
  19. package/.cursor/commands/status.md +71 -0
  20. package/.cursor/commands/suggest.md +44 -0
  21. package/.cursor/commands/test_shipit.md +197 -0
  22. package/.cursor/commands/verify.md +50 -0
  23. package/.cursor/rules/architect.mdc +84 -0
  24. package/.cursor/rules/assumption-extractor.mdc +95 -0
  25. package/.cursor/rules/docs.mdc +66 -0
  26. package/.cursor/rules/implementer.mdc +112 -0
  27. package/.cursor/rules/pm.mdc +136 -0
  28. package/.cursor/rules/qa.mdc +97 -0
  29. package/.cursor/rules/security.mdc +90 -0
  30. package/.cursor/rules/steward.mdc +99 -0
  31. package/.cursor/rules/test-runner.mdc +196 -0
  32. package/AGENTS.md +121 -0
  33. package/README.md +264 -0
  34. package/_system/architecture/CANON.md +159 -0
  35. package/_system/architecture/invariants.yml +87 -0
  36. package/_system/architecture/project-schema.json +98 -0
  37. package/_system/architecture/workflow-state-layout.md +68 -0
  38. package/_system/artifacts/SYSTEM_STATE.md +43 -0
  39. package/_system/artifacts/confidence-calibration.json +16 -0
  40. package/_system/artifacts/dependencies.md +46 -0
  41. package/_system/artifacts/framework-files-manifest.json +179 -0
  42. package/_system/artifacts/usage.json +1 -0
  43. package/_system/behaviors/DO_RELEASE.md +371 -0
  44. package/_system/behaviors/DO_RELEASE_AI.md +329 -0
  45. package/_system/behaviors/PREPARE_RELEASE.md +373 -0
  46. package/_system/behaviors/PREPARE_RELEASE_AI.md +234 -0
  47. package/_system/behaviors/WORK_ROOT_PLATFORM_ISSUES.md +140 -0
  48. package/_system/behaviors/WORK_TEST_PLAN_ISSUES.md +380 -0
  49. package/_system/do-not-repeat/abandoned-designs.md +18 -0
  50. package/_system/do-not-repeat/bad-patterns.md +19 -0
  51. package/_system/do-not-repeat/failed-experiments.md +18 -0
  52. package/_system/do-not-repeat/rejected-libraries.md +19 -0
  53. package/_system/drift/baselines.md +49 -0
  54. package/_system/drift/metrics.md +33 -0
  55. package/_system/golden-data/.gitkeep +0 -0
  56. package/_system/golden-data/README.md +47 -0
  57. package/_system/reports/mutation/mutation.html +492 -0
  58. package/_system/security/audit-allowlist.json +4 -0
  59. package/bin/create-shipit-app +29 -0
  60. package/bin/shipit +183 -0
  61. package/cli/src/commands/check.js +82 -0
  62. package/cli/src/commands/create.js +195 -0
  63. package/cli/src/commands/init.js +267 -0
  64. package/cli/src/commands/upgrade.js +196 -0
  65. package/cli/src/utils/config.js +27 -0
  66. package/cli/src/utils/file-copy.js +144 -0
  67. package/cli/src/utils/gitignore-merge.js +44 -0
  68. package/cli/src/utils/manifest.js +105 -0
  69. package/cli/src/utils/package-json-merge.js +163 -0
  70. package/cli/src/utils/project-json-merge.js +57 -0
  71. package/cli/src/utils/prompts.js +30 -0
  72. package/cli/src/utils/stack-detection.js +56 -0
  73. package/cli/src/utils/stack-files.js +364 -0
  74. package/cli/src/utils/upgrade-backup.js +159 -0
  75. package/cli/src/utils/version.js +64 -0
  76. package/dashboard-app/README.md +73 -0
  77. package/dashboard-app/eslint.config.js +23 -0
  78. package/dashboard-app/index.html +13 -0
  79. package/dashboard-app/package.json +30 -0
  80. package/dashboard-app/pnpm-lock.yaml +2721 -0
  81. package/dashboard-app/public/dashboard.json +66 -0
  82. package/dashboard-app/public/vite.svg +1 -0
  83. package/dashboard-app/src/App.css +141 -0
  84. package/dashboard-app/src/App.tsx +155 -0
  85. package/dashboard-app/src/assets/react.svg +1 -0
  86. package/dashboard-app/src/index.css +68 -0
  87. package/dashboard-app/src/main.tsx +10 -0
  88. package/dashboard-app/tsconfig.app.json +28 -0
  89. package/dashboard-app/tsconfig.json +4 -0
  90. package/dashboard-app/tsconfig.node.json +26 -0
  91. package/dashboard-app/vite.config.ts +7 -0
  92. package/package.json +116 -0
  93. package/scripts/README.md +70 -0
  94. package/scripts/audit-check.sh +125 -0
  95. package/scripts/calibration-report.sh +198 -0
  96. package/scripts/check-readiness.sh +155 -0
  97. package/scripts/collect-metrics.sh +116 -0
  98. package/scripts/command-manifest.yml +131 -0
  99. package/scripts/create-test-plan-issue.sh +110 -0
  100. package/scripts/dashboard-start.sh +16 -0
  101. package/scripts/deploy.sh +170 -0
  102. package/scripts/drift-check.sh +93 -0
  103. package/scripts/execute-rollback.sh +177 -0
  104. package/scripts/export-dashboard-json.js +208 -0
  105. package/scripts/fix-intents.sh +239 -0
  106. package/scripts/generate-dashboard.sh +136 -0
  107. package/scripts/generate-docs.sh +279 -0
  108. package/scripts/generate-project-context.sh +142 -0
  109. package/scripts/generate-release-plan.sh +443 -0
  110. package/scripts/generate-roadmap.sh +189 -0
  111. package/scripts/generate-system-state.sh +95 -0
  112. package/scripts/gh/create-intent-from-issue.sh +82 -0
  113. package/scripts/gh/create-issue-from-intent.sh +59 -0
  114. package/scripts/gh/create-pr.sh +41 -0
  115. package/scripts/gh/link-issue.sh +44 -0
  116. package/scripts/gh/on-ship-update-issue.sh +42 -0
  117. package/scripts/headless/README.md +8 -0
  118. package/scripts/headless/call-llm.js +109 -0
  119. package/scripts/headless/run-phase.sh +99 -0
  120. package/scripts/help.sh +271 -0
  121. package/scripts/init-project.sh +976 -0
  122. package/scripts/kill-intent.sh +125 -0
  123. package/scripts/lib/common.sh +29 -0
  124. package/scripts/lib/intent.sh +61 -0
  125. package/scripts/lib/progress.sh +57 -0
  126. package/scripts/lib/suggest-next.sh +131 -0
  127. package/scripts/lib/validate-intents.sh +240 -0
  128. package/scripts/lib/verify-outputs.sh +55 -0
  129. package/scripts/lib/workflow_state.sh +201 -0
  130. package/scripts/new-intent.sh +271 -0
  131. package/scripts/publish-npm.sh +28 -0
  132. package/scripts/scope-project.sh +380 -0
  133. package/scripts/setup-worktrees.sh +125 -0
  134. package/scripts/status.sh +278 -0
  135. package/scripts/suggest.sh +173 -0
  136. package/scripts/test-headless.sh +47 -0
  137. package/scripts/test-shipit.sh +52 -0
  138. package/scripts/test-workflow-state.sh +49 -0
  139. package/scripts/usage-report.sh +47 -0
  140. package/scripts/usage.sh +58 -0
  141. package/scripts/validate-cursor.sh +151 -0
  142. package/scripts/validate-project.sh +71 -0
  143. package/scripts/validate-vscode.sh +146 -0
  144. package/scripts/verify.sh +153 -0
  145. package/scripts/workflow-orchestrator.sh +97 -0
  146. package/scripts/workflow-templates/01_analysis.md.tpl +25 -0
  147. package/scripts/workflow-templates/02_plan.md.tpl +30 -0
  148. package/scripts/workflow-templates/03_implementation.md.tpl +25 -0
  149. package/scripts/workflow-templates/04_verification.md.tpl +29 -0
  150. package/scripts/workflow-templates/05_release_notes.md.tpl +16 -0
  151. package/scripts/workflow-templates/05_verification_legacy.md.tpl +6 -0
  152. package/scripts/workflow-templates/active.md.tpl +18 -0
  153. package/scripts/workflow-templates/phases.yml +39 -0
  154. package/stryker.conf.json +8 -0
  155. package/work/intent/templates/api-endpoint.md +124 -0
  156. package/work/intent/templates/bugfix.md +116 -0
  157. package/work/intent/templates/frontend-feature.md +115 -0
  158. package/work/intent/templates/generic.md +122 -0
  159. package/work/intent/templates/infra-change.md +121 -0
  160. package/work/intent/templates/refactor.md +116 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Manifest utilities - Read and parse framework-files-manifest.json
3
+ */
4
+
5
+ import { readFileSync, existsSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ /**
13
+ * Get CLI package root directory
14
+ * @returns {string} Path to CLI package root
15
+ */
16
+ function getCliPackageRoot() {
17
+ // When installed: node_modules/shipit/
18
+ // When local dev: framework root (where bin/ and cli/ are)
19
+
20
+ // Try to find node_modules/shipit from current working directory
21
+ let currentPath = process.cwd();
22
+ for (let i = 0; i < 10; i++) { // Limit depth
23
+ const nodeModulesPath = join(currentPath, 'node_modules', 'shipit');
24
+ if (existsSync(nodeModulesPath)) {
25
+ return nodeModulesPath;
26
+ }
27
+ const parent = dirname(currentPath);
28
+ if (parent === currentPath) break; // Reached root
29
+ currentPath = parent;
30
+ }
31
+
32
+ // Try relative to CLI source (local dev)
33
+ // cli/src/utils/manifest.js -> framework root
34
+ const localDevPath = join(__dirname, '..', '..', '..');
35
+ if (existsSync(join(localDevPath, '_system', 'artifacts', 'framework-files-manifest.json'))) {
36
+ return localDevPath;
37
+ }
38
+
39
+ // Try relative to bin/ (when installed globally)
40
+ // bin/shipit -> framework root
41
+ const binPath = join(__dirname, '..', '..');
42
+ if (existsSync(join(binPath, '_system', 'artifacts', 'framework-files-manifest.json'))) {
43
+ return binPath;
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Locate framework-files-manifest.json
51
+ * @returns {string} Path to manifest file
52
+ */
53
+ export function locateManifest() {
54
+ const packageRoot = getCliPackageRoot();
55
+ if (!packageRoot) {
56
+ throw new Error('ShipIt framework files not found. Reinstall: npm install -g @nlaprell/shipit');
57
+ }
58
+
59
+ const manifestPath = join(packageRoot, '_system', 'artifacts', 'framework-files-manifest.json');
60
+ if (existsSync(manifestPath)) {
61
+ return manifestPath;
62
+ }
63
+
64
+ throw new Error('ShipIt framework files not found. Reinstall: npm install -g @nlaprell/shipit');
65
+ }
66
+
67
+ /**
68
+ * Read and parse framework-files-manifest.json
69
+ * @returns {object} Parsed manifest
70
+ */
71
+ export function readManifest() {
72
+ const manifestPath = locateManifest();
73
+
74
+ try {
75
+ const content = readFileSync(manifestPath, 'utf-8');
76
+ const manifest = JSON.parse(content);
77
+
78
+ // Validate structure
79
+ if (!manifest.frameworkOwned || !manifest.userOwned || !manifest.stackSpecific || !manifest.neverCopied) {
80
+ throw new Error('Invalid manifest structure: missing required sections');
81
+ }
82
+
83
+ return manifest;
84
+ } catch (error) {
85
+ if (error.code === 'ENOENT') {
86
+ throw new Error('ShipIt framework files not found. Reinstall: npm install -g @nlaprell/shipit');
87
+ }
88
+ if (error instanceof SyntaxError) {
89
+ throw new Error(`Invalid manifest JSON: ${error.message}`);
90
+ }
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get framework root directory (where manifest is located)
97
+ * @returns {string} Framework root directory
98
+ */
99
+ export function getFrameworkRoot() {
100
+ const packageRoot = getCliPackageRoot();
101
+ if (!packageRoot) {
102
+ throw new Error('ShipIt framework files not found. Reinstall: npm install -g @nlaprell/shipit');
103
+ }
104
+ return packageRoot;
105
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Package.json merge utilities
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
6
+ import { join } from 'path';
7
+
8
+ /**
9
+ * Merge ShipIt scripts into existing package.json
10
+ * @param {string} existingPath - Path to existing package.json
11
+ * @param {object} shipitScripts - ShipIt scripts to merge
12
+ * @param {object} shipitDevDeps - ShipIt devDependencies to merge
13
+ * @param {object} options - Options (dryRun, verbose)
14
+ * @returns {object} Merge result
15
+ */
16
+ export function mergePackageJson(existingPath, shipitScripts, shipitDevDeps, options = {}) {
17
+ const { dryRun = false, verbose = false } = options;
18
+
19
+ if (!existsSync(existingPath)) {
20
+ throw new Error(`package.json not found: ${existingPath}`);
21
+ }
22
+
23
+ // Read existing package.json
24
+ let existing;
25
+ try {
26
+ const content = readFileSync(existingPath, 'utf-8');
27
+ existing = JSON.parse(content);
28
+ } catch (error) {
29
+ if (error instanceof SyntaxError) {
30
+ throw new Error(`Invalid JSON in ${existingPath}: ${error.message}`);
31
+ }
32
+ throw error;
33
+ }
34
+
35
+ // Preserve user fields
36
+ const merged = {
37
+ ...existing,
38
+ // Preserve these fields from existing
39
+ name: existing.name,
40
+ version: existing.version,
41
+ description: existing.description,
42
+ author: existing.author,
43
+ license: existing.license,
44
+ repository: existing.repository,
45
+ homepage: existing.homepage,
46
+ bugs: existing.bugs,
47
+ dependencies: existing.dependencies || {},
48
+ peerDependencies: existing.peerDependencies || {},
49
+ optionalDependencies: existing.optionalDependencies || {},
50
+ };
51
+
52
+ // Merge scripts
53
+ const existingScripts = existing.scripts || {};
54
+ const mergedScripts = { ...existingScripts };
55
+
56
+ for (const [scriptName, scriptCommand] of Object.entries(shipitScripts)) {
57
+ if (existingScripts[scriptName]) {
58
+ // Collision detected - prefix ShipIt script
59
+ const prefixedName = `shipit:${scriptName}`;
60
+ if (!existingScripts[prefixedName] && !existingScripts[`shipit:${scriptName}`]) {
61
+ mergedScripts[prefixedName] = scriptCommand;
62
+ if (verbose) {
63
+ console.log(` Script collision: ${scriptName} → ${prefixedName}`);
64
+ }
65
+ } else {
66
+ // Already prefixed or user has shipit: prefix, skip
67
+ if (verbose) {
68
+ console.log(` Skipping ${scriptName} (already customized)`);
69
+ }
70
+ }
71
+ } else {
72
+ // No collision, add directly
73
+ mergedScripts[scriptName] = scriptCommand;
74
+ }
75
+ }
76
+
77
+ merged.scripts = mergedScripts;
78
+
79
+ // Merge devDependencies
80
+ const existingDevDeps = existing.devDependencies || {};
81
+ const mergedDevDeps = { ...existingDevDeps };
82
+
83
+ for (const [depName, depVersion] of Object.entries(shipitDevDeps)) {
84
+ if (!existingDevDeps[depName]) {
85
+ // Add if missing
86
+ mergedDevDeps[depName] = depVersion;
87
+ } else {
88
+ // Keep existing version (user may have customized)
89
+ if (verbose) {
90
+ console.log(` Keeping existing devDependency: ${depName}@${existingDevDeps[depName]}`);
91
+ }
92
+ }
93
+ }
94
+
95
+ merged.devDependencies = mergedDevDeps;
96
+
97
+ // Write merged package.json
98
+ if (!dryRun) {
99
+ const formatted = JSON.stringify(merged, null, 2);
100
+ writeFileSync(existingPath, formatted + '\n', 'utf-8');
101
+ }
102
+
103
+ return {
104
+ merged,
105
+ collisions: Object.keys(shipitScripts).filter(name => existingScripts[name]),
106
+ addedScripts: Object.keys(shipitScripts).filter(name => !existingScripts[name]),
107
+ addedDevDeps: Object.keys(shipitDevDeps).filter(name => !existingDevDeps[name])
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Get ShipIt scripts template
113
+ * @returns {object} ShipIt scripts object
114
+ */
115
+ export function getShipitScripts() {
116
+ // These are the scripts that ShipIt adds to package.json
117
+ // Based on scripts/init-project.sh package.json template
118
+ return {
119
+ 'new-intent': './scripts/new-intent.sh',
120
+ 'scope-project': './scripts/scope-project.sh',
121
+ 'generate-roadmap': './scripts/generate-roadmap.sh',
122
+ 'generate-release-plan': './scripts/generate-release-plan.sh',
123
+ 'drift-check': './scripts/drift-check.sh',
124
+ 'deploy': './scripts/deploy.sh',
125
+ 'check-readiness': './scripts/check-readiness.sh',
126
+ 'workflow-orchestrator': './scripts/workflow-orchestrator.sh',
127
+ 'kill-intent': './scripts/kill-intent.sh',
128
+ 'verify': './scripts/verify.sh',
129
+ 'fix': './scripts/fix-intents.sh',
130
+ 'gh-create-issue': './scripts/gh/create-issue-from-intent.sh',
131
+ 'gh-link-issue': './scripts/gh/link-issue.sh',
132
+ 'gh-create-pr': './scripts/gh/create-pr.sh',
133
+ 'on-ship-update-issue': './scripts/gh/on-ship-update-issue.sh',
134
+ 'create-intent-from-issue': './scripts/gh/create-intent-from-issue.sh',
135
+ 'help': './scripts/help.sh',
136
+ 'status': './scripts/status.sh',
137
+ 'suggest': './scripts/suggest.sh',
138
+ 'dashboard': './scripts/dashboard-start.sh',
139
+ 'execute-rollback': './scripts/execute-rollback.sh',
140
+ 'export-dashboard-json': 'node scripts/export-dashboard-json.js'
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Get ShipIt devDependencies template
146
+ * @returns {object} ShipIt devDependencies object
147
+ */
148
+ export function getShipitDevDependencies() {
149
+ // Based on scripts/init-project.sh package.json template
150
+ return {
151
+ '@types/node': '^20.10.0',
152
+ '@typescript-eslint/eslint-plugin': '^6.15.0',
153
+ '@typescript-eslint/parser': '^6.15.0',
154
+ '@stryker-mutator/core': '^8.0.0',
155
+ '@stryker-mutator/vitest-runner': '^8.0.0',
156
+ '@vitest/coverage-v8': '^1.0.4',
157
+ 'eslint': '^8.56.0',
158
+ 'prettier': '^3.1.1',
159
+ 'tsx': '^4.7.0',
160
+ 'typescript': '^5.3.3',
161
+ 'vitest': '^1.0.4'
162
+ };
163
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * project.json merge for upgrade - preserve user fields, update shipitVersion
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
6
+ import { join } from 'path';
7
+
8
+ /**
9
+ * Merge project.json: preserve user fields, update shipitVersion and settings defaults
10
+ * @param {string} projectPath - Project root
11
+ * @param {string} newShipitVersion - Version to set
12
+ * @param {object} options - { dryRun, verbose }
13
+ * @returns {object} Merge result
14
+ */
15
+ export function mergeProjectJson(projectPath, newShipitVersion, options = {}) {
16
+ const { dryRun = false, verbose = false } = options;
17
+ const projectJsonPath = join(projectPath, 'project.json');
18
+
19
+ if (!existsSync(projectJsonPath)) {
20
+ if (verbose) console.log(' project.json not found, skipping merge');
21
+ return { merged: null, updated: false };
22
+ }
23
+
24
+ let existing;
25
+ try {
26
+ existing = JSON.parse(readFileSync(projectJsonPath, 'utf-8'));
27
+ } catch (e) {
28
+ throw new Error(`Invalid JSON in project.json: ${e.message}`);
29
+ }
30
+
31
+ const merged = {
32
+ ...existing,
33
+ name: existing.name,
34
+ description: existing.description,
35
+ version: existing.version,
36
+ techStack: existing.techStack,
37
+ created: existing.created,
38
+ highRiskDomains: existing.highRiskDomains ?? [],
39
+ shipitVersion: newShipitVersion,
40
+ settings: {
41
+ humanResponseTime: existing.settings?.humanResponseTime ?? 'minutes',
42
+ confidenceThreshold: existing.settings?.confidenceThreshold ?? 0.7,
43
+ testCoverageMinimum: existing.settings?.testCoverageMinimum ?? 80,
44
+ ...(existing.settings || {})
45
+ }
46
+ };
47
+
48
+ if (!dryRun) {
49
+ writeFileSync(
50
+ projectJsonPath,
51
+ JSON.stringify(merged, null, 2) + '\n',
52
+ 'utf-8'
53
+ );
54
+ }
55
+
56
+ return { merged, updated: true };
57
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Prompt utilities for interactive mode
3
+ */
4
+
5
+ import { createInterface } from 'readline';
6
+
7
+ /**
8
+ * Create readline interface
9
+ * @returns {object} Readline interface
10
+ */
11
+ export function createReadlineInterface() {
12
+ return createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Prompt user for input
20
+ * @param {object} rl - Readline interface
21
+ * @param {string} question - Question to ask
22
+ * @returns {Promise<string>} User input
23
+ */
24
+ export function promptUser(rl, question) {
25
+ return new Promise((resolve) => {
26
+ rl.question(question, (answer) => {
27
+ resolve(answer);
28
+ });
29
+ });
30
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Stack detection utilities
3
+ */
4
+
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import { join } from 'path';
7
+
8
+ /**
9
+ * Detect tech stack from existing files
10
+ * @param {string} projectPath - Path to project directory
11
+ * @returns {string|null} Detected stack or null if ambiguous/unknown
12
+ */
13
+ export function detectTechStack(projectPath) {
14
+ const packageJsonPath = join(projectPath, 'package.json');
15
+ const pyprojectTomlPath = join(projectPath, 'pyproject.toml');
16
+ const requirementsTxtPath = join(projectPath, 'requirements.txt');
17
+ const setupPyPath = join(projectPath, 'setup.py');
18
+
19
+ const hasPackageJson = existsSync(packageJsonPath);
20
+ const hasPyprojectToml = existsSync(pyprojectTomlPath);
21
+ const hasRequirementsTxt = existsSync(requirementsTxtPath);
22
+ const hasSetupPy = existsSync(setupPyPath);
23
+
24
+ // Check for ambiguous case (both Node and Python indicators)
25
+ if (hasPackageJson && (hasPyprojectToml || hasRequirementsTxt || hasSetupPy)) {
26
+ return null; // Ambiguous, prompt user
27
+ }
28
+
29
+ // Validate package.json if it exists
30
+ if (hasPackageJson) {
31
+ try {
32
+ const content = readFileSync(packageJsonPath, 'utf-8');
33
+ JSON.parse(content); // Validate JSON
34
+ return 'typescript-nodejs';
35
+ } catch (e) {
36
+ return null; // Invalid JSON, prompt user
37
+ }
38
+ }
39
+
40
+ // Check for Python indicators
41
+ if (hasPyprojectToml || hasRequirementsTxt || hasSetupPy) {
42
+ return 'python';
43
+ }
44
+
45
+ // Empty directory or no indicators
46
+ return null; // Prompt user
47
+ }
48
+
49
+ /**
50
+ * Validate stack value
51
+ * @param {string} stack - Stack value to validate
52
+ * @returns {boolean} True if valid
53
+ */
54
+ export function isValidStack(stack) {
55
+ return ['typescript-nodejs', 'python', 'other'].includes(stack);
56
+ }