quidproquo-cli 0.1.6 → 0.1.8

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 (138) hide show
  1. package/docker/dev-server/Dockerfile +7 -9
  2. package/lib/commonjs/cli/commandMenu.d.ts +2 -0
  3. package/lib/commonjs/cli/commandMenu.js +81 -0
  4. package/lib/commonjs/cli/commandMenu.js.map +1 -0
  5. package/lib/commonjs/cli/commandRegistry.d.ts +10 -0
  6. package/lib/commonjs/cli/commandRegistry.js +131 -0
  7. package/lib/commonjs/cli/commandRegistry.js.map +1 -0
  8. package/lib/commonjs/cli/runCli.js +49 -49
  9. package/lib/commonjs/cli/runCli.js.map +1 -1
  10. package/lib/commonjs/commands/clearResources.d.ts +1 -0
  11. package/lib/commonjs/commands/clearResources.js +36 -0
  12. package/lib/commonjs/commands/clearResources.js.map +1 -0
  13. package/lib/commonjs/commands/go.js +3 -1
  14. package/lib/commonjs/commands/go.js.map +1 -1
  15. package/lib/commonjs/commands/goDev.js +23 -122
  16. package/lib/commonjs/commands/goDev.js.map +1 -1
  17. package/lib/commonjs/commands/goDevApi.d.ts +1 -0
  18. package/lib/commonjs/commands/goDevApi.js +110 -0
  19. package/lib/commonjs/commands/goDevApi.js.map +1 -0
  20. package/lib/commonjs/commands/goDevWeb.d.ts +4 -1
  21. package/lib/commonjs/commands/goDevWeb.js +4 -2
  22. package/lib/commonjs/commands/goDevWeb.js.map +1 -1
  23. package/lib/commonjs/commands/goDocker.js +3 -1
  24. package/lib/commonjs/commands/goDocker.js.map +1 -1
  25. package/lib/commonjs/commands/hooks.js +23 -5
  26. package/lib/commonjs/commands/hooks.js.map +1 -1
  27. package/lib/commonjs/lib/clearResourcesPlan.d.ts +15 -0
  28. package/lib/commonjs/lib/clearResourcesPlan.js +80 -0
  29. package/lib/commonjs/lib/clearResourcesPlan.js.map +1 -0
  30. package/lib/commonjs/lib/deployPrompts.d.ts +1 -0
  31. package/lib/commonjs/lib/deployPrompts.js +40 -2
  32. package/lib/commonjs/lib/deployPrompts.js.map +1 -1
  33. package/lib/commonjs/lib/devServerEntry.js +3 -0
  34. package/lib/commonjs/lib/devServerEntry.js.map +1 -1
  35. package/lib/commonjs/lib/hooks.d.ts +2 -1
  36. package/lib/commonjs/lib/hooks.js +100 -5
  37. package/lib/commonjs/lib/hooks.js.map +1 -1
  38. package/lib/commonjs/lib/killStaleListeners.d.ts +3 -0
  39. package/lib/commonjs/lib/killStaleListeners.js +170 -0
  40. package/lib/commonjs/lib/killStaleListeners.js.map +1 -0
  41. package/lib/commonjs/lib/prompts.d.ts +10 -0
  42. package/lib/commonjs/lib/prompts.js +15 -1
  43. package/lib/commonjs/lib/prompts.js.map +1 -1
  44. package/lib/commonjs/lib/qpqConfigs.d.ts +1 -0
  45. package/lib/commonjs/lib/qpqConfigs.js +9 -1
  46. package/lib/commonjs/lib/qpqConfigs.js.map +1 -1
  47. package/lib/commonjs/lib/resolveAppSelection.js +1 -1
  48. package/lib/commonjs/lib/resolveAppSelection.js.map +1 -1
  49. package/lib/commonjs/lib/runTasks.d.ts +5 -0
  50. package/lib/commonjs/lib/runTasks.js +82 -1
  51. package/lib/commonjs/lib/runTasks.js.map +1 -1
  52. package/lib/commonjs/lib/typeWatcher.d.ts +2 -0
  53. package/lib/commonjs/lib/typeWatcher.js +59 -0
  54. package/lib/commonjs/lib/typeWatcher.js.map +1 -0
  55. package/lib/commonjs/platforms/aws/cdkApp.js +5 -1
  56. package/lib/commonjs/platforms/aws/cdkApp.js.map +1 -1
  57. package/lib/commonjs/platforms/aws/clearResources.d.ts +2 -0
  58. package/lib/commonjs/platforms/aws/clearResources.js +167 -0
  59. package/lib/commonjs/platforms/aws/clearResources.js.map +1 -0
  60. package/lib/commonjs/platforms/aws/go.js +15 -1
  61. package/lib/commonjs/platforms/aws/go.js.map +1 -1
  62. package/lib/commonjs/platforms/aws/goDocker.js +76 -8
  63. package/lib/commonjs/platforms/aws/goDocker.js.map +1 -1
  64. package/lib/commonjs/platforms/aws/index.js +2 -0
  65. package/lib/commonjs/platforms/aws/index.js.map +1 -1
  66. package/lib/commonjs/platforms/docker/go.js +2 -2
  67. package/lib/commonjs/platforms/docker/go.js.map +1 -1
  68. package/lib/commonjs/platforms/types.d.ts +2 -0
  69. package/lib/commonjs/platforms/types.js.map +1 -1
  70. package/lib/esm/cli/commandMenu.d.ts +2 -0
  71. package/lib/esm/cli/commandMenu.js +67 -0
  72. package/lib/esm/cli/commandMenu.js.map +1 -0
  73. package/lib/esm/cli/commandRegistry.d.ts +10 -0
  74. package/lib/esm/cli/commandRegistry.js +115 -0
  75. package/lib/esm/cli/commandRegistry.js.map +1 -0
  76. package/lib/esm/cli/runCli.js +49 -49
  77. package/lib/esm/cli/runCli.js.map +1 -1
  78. package/lib/esm/commands/clearResources.d.ts +1 -0
  79. package/lib/esm/commands/clearResources.js +23 -0
  80. package/lib/esm/commands/clearResources.js.map +1 -0
  81. package/lib/esm/commands/go.js +3 -2
  82. package/lib/esm/commands/go.js.map +1 -1
  83. package/lib/esm/commands/goDev.js +24 -120
  84. package/lib/esm/commands/goDev.js.map +1 -1
  85. package/lib/esm/commands/goDevApi.d.ts +1 -0
  86. package/lib/esm/commands/goDevApi.js +94 -0
  87. package/lib/esm/commands/goDevApi.js.map +1 -0
  88. package/lib/esm/commands/goDevWeb.d.ts +4 -1
  89. package/lib/esm/commands/goDevWeb.js +4 -2
  90. package/lib/esm/commands/goDevWeb.js.map +1 -1
  91. package/lib/esm/commands/goDocker.js +3 -2
  92. package/lib/esm/commands/goDocker.js.map +1 -1
  93. package/lib/esm/commands/hooks.js +21 -6
  94. package/lib/esm/commands/hooks.js.map +1 -1
  95. package/lib/esm/lib/clearResourcesPlan.d.ts +15 -0
  96. package/lib/esm/lib/clearResourcesPlan.js +66 -0
  97. package/lib/esm/lib/clearResourcesPlan.js.map +1 -0
  98. package/lib/esm/lib/deployPrompts.d.ts +1 -0
  99. package/lib/esm/lib/deployPrompts.js +38 -1
  100. package/lib/esm/lib/deployPrompts.js.map +1 -1
  101. package/lib/esm/lib/devServerEntry.js +3 -0
  102. package/lib/esm/lib/devServerEntry.js.map +1 -1
  103. package/lib/esm/lib/hooks.d.ts +2 -1
  104. package/lib/esm/lib/hooks.js +97 -5
  105. package/lib/esm/lib/hooks.js.map +1 -1
  106. package/lib/esm/lib/killStaleListeners.d.ts +3 -0
  107. package/lib/esm/lib/killStaleListeners.js +161 -0
  108. package/lib/esm/lib/killStaleListeners.js.map +1 -0
  109. package/lib/esm/lib/prompts.d.ts +10 -0
  110. package/lib/esm/lib/prompts.js +12 -0
  111. package/lib/esm/lib/prompts.js.map +1 -1
  112. package/lib/esm/lib/qpqConfigs.d.ts +1 -0
  113. package/lib/esm/lib/qpqConfigs.js +7 -0
  114. package/lib/esm/lib/qpqConfigs.js.map +1 -1
  115. package/lib/esm/lib/resolveAppSelection.js +1 -1
  116. package/lib/esm/lib/resolveAppSelection.js.map +1 -1
  117. package/lib/esm/lib/runTasks.d.ts +5 -0
  118. package/lib/esm/lib/runTasks.js +80 -0
  119. package/lib/esm/lib/runTasks.js.map +1 -1
  120. package/lib/esm/lib/typeWatcher.d.ts +2 -0
  121. package/lib/esm/lib/typeWatcher.js +51 -0
  122. package/lib/esm/lib/typeWatcher.js.map +1 -0
  123. package/lib/esm/platforms/aws/cdkApp.js +5 -1
  124. package/lib/esm/platforms/aws/cdkApp.js.map +1 -1
  125. package/lib/esm/platforms/aws/clearResources.d.ts +2 -0
  126. package/lib/esm/platforms/aws/clearResources.js +151 -0
  127. package/lib/esm/platforms/aws/clearResources.js.map +1 -0
  128. package/lib/esm/platforms/aws/go.js +16 -2
  129. package/lib/esm/platforms/aws/go.js.map +1 -1
  130. package/lib/esm/platforms/aws/goDocker.js +77 -9
  131. package/lib/esm/platforms/aws/goDocker.js.map +1 -1
  132. package/lib/esm/platforms/aws/index.js +2 -0
  133. package/lib/esm/platforms/aws/index.js.map +1 -1
  134. package/lib/esm/platforms/docker/go.js +2 -2
  135. package/lib/esm/platforms/docker/go.js.map +1 -1
  136. package/lib/esm/platforms/types.d.ts +2 -0
  137. package/lib/esm/platforms/types.js.map +1 -1
  138. package/package.json +12 -10
@@ -1,7 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { getAppDirectory, getAvailableApps } from './discovery';
4
- import { runCommand } from './runCommand';
3
+ import { getAppDirectory, getAvailableApps, getRoot } from './discovery';
4
+ import { nextPrefixColor, runCommand, runCommandPrefixed } from './runCommand';
5
+ import { runDependencyTasks } from './runTasks';
5
6
  // App lifecycle hooks: apps/<app>/package.json scripts named `qpq:<hook>`
6
7
  // (e.g. qpq:postinstall, qpq:prebuild). The CLI invokes them when present;
7
8
  // apps without the hook are skipped silently.
@@ -19,9 +20,100 @@ export const runAppHook = async (appName, hook) => {
19
20
  await runCommand('npm', ['run', scriptName], { cwd: getAppDirectory(appName) });
20
21
  return true;
21
22
  };
22
- export const runAppHookForAllApps = async (hook) => {
23
- for (const appName of getAvailableApps()) {
24
- await runAppHook(appName, hook);
23
+ const isDirectory = (candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory();
24
+ const listSubdirectories = (dir) => {
25
+ if (!isDirectory(dir)) {
26
+ return [];
25
27
  }
28
+ return fs
29
+ .readdirSync(dir)
30
+ .map((name) => path.join(dir, name))
31
+ .filter(isDirectory);
32
+ };
33
+ // Expand one `workspaces` glob. Only literal segments and bare `*` are
34
+ // supported, which covers the patterns qpq repos use (e.g.
35
+ // "apps/*/services/*/models"); anything fancier ("**", braces) matches nothing.
36
+ const expandWorkspacePattern = (root, pattern) => {
37
+ let dirs = [root];
38
+ for (const segment of pattern.split('/')) {
39
+ if (segment === '*') {
40
+ dirs = dirs.flatMap(listSubdirectories);
41
+ }
42
+ else {
43
+ dirs = dirs.map((dir) => path.join(dir, segment)).filter(isDirectory);
44
+ }
45
+ }
46
+ return dirs;
47
+ };
48
+ const readPackageJson = (dir) => {
49
+ const packageJsonPath = path.join(dir, 'package.json');
50
+ if (!fs.existsSync(packageJsonPath)) {
51
+ return undefined;
52
+ }
53
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
54
+ };
55
+ // Every directory that can carry hooks: app dirs plus all workspace packages,
56
+ // deduped (an app dir could itself be a workspace).
57
+ const getHookPackages = () => {
58
+ const root = getRoot();
59
+ const rootPkg = readPackageJson(root);
60
+ const appDirs = getAvailableApps().map((appName) => ({ dir: getAppDirectory(appName), fallbackLabel: appName }));
61
+ const workspaceDirs = (rootPkg?.workspaces ?? [])
62
+ .flatMap((pattern) => expandWorkspacePattern(root, pattern))
63
+ .map((dir) => ({ dir, fallbackLabel: path.relative(root, dir) }));
64
+ const packages = [];
65
+ const seen = new Set();
66
+ for (const { dir, fallbackLabel } of [...appDirs, ...workspaceDirs]) {
67
+ const resolved = path.resolve(dir);
68
+ if (seen.has(resolved)) {
69
+ continue;
70
+ }
71
+ seen.add(resolved);
72
+ const pkg = readPackageJson(dir);
73
+ if (pkg) {
74
+ packages.push({ dir, label: pkg.name ?? fallbackLabel, pkg });
75
+ }
76
+ }
77
+ return packages;
78
+ };
79
+ // The script a hook resolves to in one package: `qpq:<hook>` when defined,
80
+ // otherwise the plain `<hook>` script. Empty-string scripts count as absent.
81
+ const resolveHookScript = (pkg, hook) => {
82
+ if (pkg.scripts?.[`qpq:${hook}`]) {
83
+ return `qpq:${hook}`;
84
+ }
85
+ if (pkg.scripts?.[hook]) {
86
+ return hook;
87
+ }
88
+ return undefined;
89
+ };
90
+ // Run the hook's script in every app/workspace package that defines it
91
+ // (qpq:<hook> preferred over a plain <hook> script), in dependency order
92
+ // (declared deps resolved by package name), with up to `jobs` running
93
+ // concurrently. Packages whose dependencies aren't part of this run don't
94
+ // wait on them. Returns the failures; callers decide fatality.
95
+ export const runHookForAllPackages = async (hook, jobs) => {
96
+ const hookPackages = getHookPackages()
97
+ .map((candidate) => ({ ...candidate, scriptName: resolveHookScript(candidate.pkg, hook) }))
98
+ .filter((candidate) => candidate.scriptName !== undefined);
99
+ if (hookPackages.length === 0) {
100
+ console.log(`No app or workspace package defines a "qpq:${hook}" or "${hook}" script, nothing to run.`);
101
+ return [];
102
+ }
103
+ // Dependency edges only count between packages that are part of this run:
104
+ // a dep that doesn't define the hook has nothing to wait for.
105
+ const runSetNames = new Set(hookPackages.map((hookPackage) => hookPackage.pkg.name).filter(Boolean));
106
+ const tasks = hookPackages.map((hookPackage) => {
107
+ const declared = { ...hookPackage.pkg.dependencies, ...hookPackage.pkg.devDependencies, ...hookPackage.pkg.peerDependencies };
108
+ const deps = Object.keys(declared).filter((name) => runSetNames.has(name) && name !== hookPackage.pkg.name);
109
+ return {
110
+ id: hookPackage.pkg.name ?? hookPackage.dir,
111
+ label: hookPackage.label,
112
+ deps,
113
+ run: () => runCommandPrefixed(hookPackage.label, 'npm', ['run', hookPackage.scriptName], { cwd: hookPackage.dir, color: nextPrefixColor() }),
114
+ };
115
+ });
116
+ console.log(`Running ${hook} hooks in ${tasks.length} package(s) (${jobs} jobs, dependency ordered)`);
117
+ return runDependencyTasks(tasks, jobs);
26
118
  };
27
119
  //# sourceMappingURL=hooks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../../src/lib/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,0EAA0E;AAC1E,2EAA2E;AAC3E,8CAA8C;AAC9C,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,OAAe,EAAE,IAAY,EAAoB,EAAE;IAClF,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC;IAC5E,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QACpC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1E,MAAM,UAAU,GAAG,OAAO,IAAI,EAAE,CAAC;IACjC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;QACvC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,WAAW,UAAU,SAAS,OAAO,GAAG,CAAC,CAAC;IACtD,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,EAAE,GAAG,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAChF,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EAAE,IAAY,EAAiB,EAAE;IACxE,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,EAAE,CAAC;QACzC,MAAM,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAClC,CAAC;AACH,CAAC,CAAC","sourcesContent":["import fs from 'fs';\nimport path from 'path';\n\nimport { getAppDirectory, getAvailableApps } from './discovery';\nimport { runCommand } from './runCommand';\n\n// App lifecycle hooks: apps/<app>/package.json scripts named `qpq:<hook>`\n// (e.g. qpq:postinstall, qpq:prebuild). The CLI invokes them when present;\n// apps without the hook are skipped silently.\nexport const runAppHook = async (appName: string, hook: string): Promise<boolean> => {\n const packageJsonPath = path.join(getAppDirectory(appName), 'package.json');\n if (!fs.existsSync(packageJsonPath)) {\n return false;\n }\n\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n const scriptName = `qpq:${hook}`;\n if (!packageJson.scripts?.[scriptName]) {\n return false;\n }\n\n console.log(`Running ${scriptName} for [${appName}]`);\n await runCommand('npm', ['run', scriptName], { cwd: getAppDirectory(appName) });\n return true;\n};\n\nexport const runAppHookForAllApps = async (hook: string): Promise<void> => {\n for (const appName of getAvailableApps()) {\n await runAppHook(appName, hook);\n }\n};\n"]}
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../../src/lib/hooks.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs';\nimport path from 'path';\n\nimport { getAppDirectory, getAvailableApps, getRoot } from './discovery';\nimport { nextPrefixColor, runCommand, runCommandPrefixed } from './runCommand';\nimport { runDependencyTasks, TaskFailure } from './runTasks';\n\ninterface HookPackageJson {\n name?: string;\n scripts?: Record<string, string>;\n workspaces?: string[];\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n}\n\n// A directory whose package.json can carry qpq:<hook> scripts: either an app\n// (apps/<app>/package.json, which need not be an npm workspace) or any package\n// matched by the consumer root's `workspaces` globs.\ninterface HookPackage {\n dir: string;\n label: string; // app name or package name, for display\n pkg: HookPackageJson;\n}\n\n// App lifecycle hooks: apps/<app>/package.json scripts named `qpq:<hook>`\n// (e.g. qpq:postinstall, qpq:prebuild). The CLI invokes them when present;\n// apps without the hook are skipped silently.\nexport const runAppHook = async (appName: string, hook: string): Promise<boolean> => {\n const packageJsonPath = path.join(getAppDirectory(appName), 'package.json');\n if (!fs.existsSync(packageJsonPath)) {\n return false;\n }\n\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n const scriptName = `qpq:${hook}`;\n if (!packageJson.scripts?.[scriptName]) {\n return false;\n }\n\n console.log(`Running ${scriptName} for [${appName}]`);\n await runCommand('npm', ['run', scriptName], { cwd: getAppDirectory(appName) });\n return true;\n};\n\nconst isDirectory = (candidate: string): boolean => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory();\n\nconst listSubdirectories = (dir: string): string[] => {\n if (!isDirectory(dir)) {\n return [];\n }\n return fs\n .readdirSync(dir)\n .map((name) => path.join(dir, name))\n .filter(isDirectory);\n};\n\n// Expand one `workspaces` glob. Only literal segments and bare `*` are\n// supported, which covers the patterns qpq repos use (e.g.\n// \"apps/*/services/*/models\"); anything fancier (\"**\", braces) matches nothing.\nconst expandWorkspacePattern = (root: string, pattern: string): string[] => {\n let dirs = [root];\n for (const segment of pattern.split('/')) {\n if (segment === '*') {\n dirs = dirs.flatMap(listSubdirectories);\n } else {\n dirs = dirs.map((dir) => path.join(dir, segment)).filter(isDirectory);\n }\n }\n return dirs;\n};\n\nconst readPackageJson = (dir: string): HookPackageJson | undefined => {\n const packageJsonPath = path.join(dir, 'package.json');\n if (!fs.existsSync(packageJsonPath)) {\n return undefined;\n }\n return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as HookPackageJson;\n};\n\n// Every directory that can carry hooks: app dirs plus all workspace packages,\n// deduped (an app dir could itself be a workspace).\nconst getHookPackages = (): HookPackage[] => {\n const root = getRoot();\n const rootPkg = readPackageJson(root);\n\n const appDirs = getAvailableApps().map((appName) => ({ dir: getAppDirectory(appName), fallbackLabel: appName }));\n const workspaceDirs = (rootPkg?.workspaces ?? [])\n .flatMap((pattern) => expandWorkspacePattern(root, pattern))\n .map((dir) => ({ dir, fallbackLabel: path.relative(root, dir) }));\n\n const packages: HookPackage[] = [];\n const seen = new Set<string>();\n for (const { dir, fallbackLabel } of [...appDirs, ...workspaceDirs]) {\n const resolved = path.resolve(dir);\n if (seen.has(resolved)) {\n continue;\n }\n seen.add(resolved);\n\n const pkg = readPackageJson(dir);\n if (pkg) {\n packages.push({ dir, label: pkg.name ?? fallbackLabel, pkg });\n }\n }\n\n return packages;\n};\n\n// The script a hook resolves to in one package: `qpq:<hook>` when defined,\n// otherwise the plain `<hook>` script. Empty-string scripts count as absent.\nconst resolveHookScript = (pkg: HookPackageJson, hook: string): string | undefined => {\n if (pkg.scripts?.[`qpq:${hook}`]) {\n return `qpq:${hook}`;\n }\n if (pkg.scripts?.[hook]) {\n return hook;\n }\n return undefined;\n};\n\n// Run the hook's script in every app/workspace package that defines it\n// (qpq:<hook> preferred over a plain <hook> script), in dependency order\n// (declared deps resolved by package name), with up to `jobs` running\n// concurrently. Packages whose dependencies aren't part of this run don't\n// wait on them. Returns the failures; callers decide fatality.\nexport const runHookForAllPackages = async (hook: string, jobs: number): Promise<TaskFailure[]> => {\n const hookPackages = getHookPackages()\n .map((candidate) => ({ ...candidate, scriptName: resolveHookScript(candidate.pkg, hook) }))\n .filter((candidate): candidate is HookPackage & { scriptName: string } => candidate.scriptName !== undefined);\n\n if (hookPackages.length === 0) {\n console.log(`No app or workspace package defines a \"qpq:${hook}\" or \"${hook}\" script, nothing to run.`);\n return [];\n }\n\n // Dependency edges only count between packages that are part of this run:\n // a dep that doesn't define the hook has nothing to wait for.\n const runSetNames = new Set(hookPackages.map((hookPackage) => hookPackage.pkg.name).filter(Boolean));\n\n const tasks = hookPackages.map((hookPackage) => {\n const declared = { ...hookPackage.pkg.dependencies, ...hookPackage.pkg.devDependencies, ...hookPackage.pkg.peerDependencies };\n const deps = Object.keys(declared).filter((name) => runSetNames.has(name) && name !== hookPackage.pkg.name);\n\n return {\n id: hookPackage.pkg.name ?? hookPackage.dir,\n label: hookPackage.label,\n deps,\n run: () => runCommandPrefixed(hookPackage.label, 'npm', ['run', hookPackage.scriptName], { cwd: hookPackage.dir, color: nextPrefixColor() }),\n };\n });\n\n console.log(`Running ${hook} hooks in ${tasks.length} package(s) (${jobs} jobs, dependency ordered)`);\n return runDependencyTasks(tasks, jobs);\n};\n"]}
@@ -0,0 +1,3 @@
1
+ export declare const isQpqCliCommand: (command: string) => boolean;
2
+ export declare const killOtherQpqDevProcesses: (root: string) => void;
3
+ export declare const killStaleListeners: (ports: number[], isOurs: (command: string) => boolean) => void;
@@ -0,0 +1,161 @@
1
+ // Pre-start sweep shared by the dev-server commands: a previous run whose
2
+ // wrapper died without killing its listener (closed terminal, SIGKILL) leaves
3
+ // an orphaned process holding a dev port, and the next launch dies with
4
+ // EADDRINUSE. Before starting, kill any listener on the given ports whose
5
+ // command line matches `isOurs`. Anything else on the ports is left alone
6
+ // and reported — it's not ours.
7
+ import { execSync } from 'child_process';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ // Matches any invocation of the qpq CLI itself, regardless of how it was
11
+ // launched: `node node_modules/.bin/qpq ...` (the npm-script shim, which has
12
+ // no .js extension), a direct `node .../lib/commonjs/bin/qpq.js` call, a
13
+ // global `qpq` shebang script, etc. Checks each whitespace-separated token's
14
+ // basename (extension stripped) rather than a fixed substring like
15
+ // 'bin/qpq.js', which only matches the last of those forms.
16
+ export const isQpqCliCommand = (command) => command
17
+ .trim()
18
+ .split(/\s+/)
19
+ .some((token) => {
20
+ const base = path.basename(token);
21
+ const withoutExt = base.endsWith('.js') ? base.slice(0, -3) : base;
22
+ return withoutExt === 'qpq';
23
+ });
24
+ // SIGTERM only requests termination — the caller (about to bind this same
25
+ // process's ports itself) needs it actually gone first. A stale dev server
26
+ // can be mid-startup when killed (still resolving config, not yet bound to
27
+ // every port it eventually binds) and keep running for a bit past the
28
+ // signal, going on to bind a LATER port — waiting for just the one port that
29
+ // matched isn't enough; wait for the whole pid to disappear so it can't bind
30
+ // anything else afterward. Poll synchronously (this whole sweep runs before
31
+ // anything async starts) via `kill(pid, 0)` — reliable and fast here because
32
+ // these are unrelated, previously-orphaned processes, not children of this
33
+ // one (a real child can sit as a zombie answering `kill(pid, 0)` until this
34
+ // process's own event loop reaps it, which a tight sync loop would starve;
35
+ // doesn't apply to a process we didn't spawn).
36
+ const waitForPidGone = (pid, timeoutMs = 3000) => {
37
+ const deadline = Date.now() + timeoutMs;
38
+ while (Date.now() < deadline) {
39
+ try {
40
+ process.kill(pid, 0);
41
+ }
42
+ catch {
43
+ return; // throws once the pid no longer exists
44
+ }
45
+ execSync('sleep 0.05');
46
+ }
47
+ };
48
+ // realpath'd: lsof reports the canonical path (e.g. /private/tmp on macOS,
49
+ // where /tmp is a symlink), so comparing against a caller-supplied root
50
+ // verbatim would false-negative whenever any component of the repo path is
51
+ // itself a symlink (a symlinked home directory, an NFS mount, etc).
52
+ const getProcessCwd = (pid) => {
53
+ try {
54
+ const line = execSync(`lsof -a -p ${pid} -d cwd -Fn`, { stdio: ['ignore', 'pipe', 'ignore'] })
55
+ .toString()
56
+ .split('\n')
57
+ .find((l) => l.startsWith('n'));
58
+ return line ? fs.realpathSync(line.slice(1)) : null;
59
+ }
60
+ catch {
61
+ return null; // pid gone, or we don't have permission to inspect it
62
+ }
63
+ };
64
+ // The sweep below must never target this process's own wrappers: launched as
65
+ // `npx qpq go:dev:api` (or via any shell whose -c string names qpq), the
66
+ // ancestor chain (`sh -c "npx ... qpq ..."`, `npm exec qpq ...`) matches
67
+ // isQpqCliCommand and shares our cwd, so excluding just process.pid isn't
68
+ // enough — SIGINTing an ancestor tears down our own tree. Walk ppid to the
69
+ // top and exclude the whole chain.
70
+ const getOwnAncestorPids = () => {
71
+ const ancestors = new Set();
72
+ let pid = process.pid;
73
+ while (pid > 1 && !ancestors.has(pid)) {
74
+ ancestors.add(pid);
75
+ try {
76
+ pid = Number(execSync(`ps -o ppid= -p ${pid}`, { stdio: ['ignore', 'pipe', 'ignore'] })
77
+ .toString()
78
+ .trim());
79
+ }
80
+ catch {
81
+ break;
82
+ }
83
+ }
84
+ return ancestors;
85
+ };
86
+ // killStaleListeners only sees processes currently BOUND to one of the given
87
+ // ports — it can't catch a still-running `qpq go:dev*` watcher whose spawned
88
+ // dev-server child already died (a crash, or us killing just the child):
89
+ // the watcher itself holds no port at that moment, so it's invisible to a
90
+ // port scan, yet it's still alive and can spawn a fresh child — and bind a
91
+ // port — moments later, racing whatever we're about to start. Close that gap
92
+ // by finding any OTHER qpq CLI process rooted in the same repo checkout
93
+ // (matched by cwd, not by port or command-line args, which vary by how it
94
+ // was invoked) and killing it outright, regardless of what it currently has
95
+ // bound. SIGINT (not SIGTERM) so goDevApiCommand/goDevWebCommand's own
96
+ // handler runs and tears down its child cleanly rather than orphaning it.
97
+ export const killOtherQpqDevProcesses = (root) => {
98
+ const resolvedRoot = fs.realpathSync(root);
99
+ const ownPids = getOwnAncestorPids();
100
+ let lines = [];
101
+ try {
102
+ lines = execSync('ps -axo pid=,command=', { stdio: ['ignore', 'pipe', 'ignore'] })
103
+ .toString()
104
+ .split('\n');
105
+ }
106
+ catch {
107
+ return;
108
+ }
109
+ for (const line of lines) {
110
+ const trimmed = line.trim();
111
+ if (!trimmed)
112
+ continue;
113
+ const spaceIndex = trimmed.indexOf(' ');
114
+ if (spaceIndex === -1)
115
+ continue;
116
+ const pid = Number(trimmed.slice(0, spaceIndex));
117
+ const command = trimmed.slice(spaceIndex + 1).trim();
118
+ if (!pid || ownPids.has(pid) || !isQpqCliCommand(command))
119
+ continue;
120
+ if (getProcessCwd(pid) !== resolvedRoot)
121
+ continue;
122
+ console.log(`Killing other qpq dev process for this repo (pid ${pid}): ${command}`);
123
+ process.kill(pid, 'SIGINT');
124
+ waitForPidGone(pid);
125
+ }
126
+ };
127
+ export const killStaleListeners = (ports, isOurs) => {
128
+ for (const port of ports) {
129
+ let pids = [];
130
+ try {
131
+ pids = execSync(`lsof -t -iTCP:${port} -sTCP:LISTEN`, {
132
+ stdio: ['ignore', 'pipe', 'ignore'],
133
+ })
134
+ .toString()
135
+ .split('\n')
136
+ .map((line) => Number(line.trim()))
137
+ .filter(Boolean);
138
+ }
139
+ catch {
140
+ continue; // lsof exits non-zero when nothing is listening
141
+ }
142
+ for (const pid of pids) {
143
+ let command = '';
144
+ try {
145
+ command = execSync(`ps -p ${pid} -o command=`).toString().trim();
146
+ }
147
+ catch {
148
+ continue; // already gone
149
+ }
150
+ if (isOurs(command)) {
151
+ console.log(`Killing stale dev server on port ${port} (pid ${pid})`);
152
+ process.kill(pid);
153
+ waitForPidGone(pid);
154
+ }
155
+ else {
156
+ console.warn(`Port ${port} is in use by an unrelated process (pid ${pid}: ${command}) — not killing it.`);
157
+ }
158
+ }
159
+ }
160
+ };
161
+ //# sourceMappingURL=killStaleListeners.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"killStaleListeners.js","sourceRoot":"","sources":["../../../src/lib/killStaleListeners.ts"],"names":[],"mappings":"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","sourcesContent":["// Pre-start sweep shared by the dev-server commands: a previous run whose\n// wrapper died without killing its listener (closed terminal, SIGKILL) leaves\n// an orphaned process holding a dev port, and the next launch dies with\n// EADDRINUSE. Before starting, kill any listener on the given ports whose\n// command line matches `isOurs`. Anything else on the ports is left alone\n// and reported — it's not ours.\nimport { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\n\n// Matches any invocation of the qpq CLI itself, regardless of how it was\n// launched: `node node_modules/.bin/qpq ...` (the npm-script shim, which has\n// no .js extension), a direct `node .../lib/commonjs/bin/qpq.js` call, a\n// global `qpq` shebang script, etc. Checks each whitespace-separated token's\n// basename (extension stripped) rather than a fixed substring like\n// 'bin/qpq.js', which only matches the last of those forms.\nexport const isQpqCliCommand = (command: string): boolean =>\n command\n .trim()\n .split(/\\s+/)\n .some((token) => {\n const base = path.basename(token);\n const withoutExt = base.endsWith('.js') ? base.slice(0, -3) : base;\n return withoutExt === 'qpq';\n });\n\n// SIGTERM only requests termination — the caller (about to bind this same\n// process's ports itself) needs it actually gone first. A stale dev server\n// can be mid-startup when killed (still resolving config, not yet bound to\n// every port it eventually binds) and keep running for a bit past the\n// signal, going on to bind a LATER port — waiting for just the one port that\n// matched isn't enough; wait for the whole pid to disappear so it can't bind\n// anything else afterward. Poll synchronously (this whole sweep runs before\n// anything async starts) via `kill(pid, 0)` — reliable and fast here because\n// these are unrelated, previously-orphaned processes, not children of this\n// one (a real child can sit as a zombie answering `kill(pid, 0)` until this\n// process's own event loop reaps it, which a tight sync loop would starve;\n// doesn't apply to a process we didn't spawn).\nconst waitForPidGone = (pid: number, timeoutMs = 3000): void => {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n try {\n process.kill(pid, 0);\n } catch {\n return; // throws once the pid no longer exists\n }\n execSync('sleep 0.05');\n }\n};\n\n// realpath'd: lsof reports the canonical path (e.g. /private/tmp on macOS,\n// where /tmp is a symlink), so comparing against a caller-supplied root\n// verbatim would false-negative whenever any component of the repo path is\n// itself a symlink (a symlinked home directory, an NFS mount, etc).\nconst getProcessCwd = (pid: number): string | null => {\n try {\n const line = execSync(`lsof -a -p ${pid} -d cwd -Fn`, { stdio: ['ignore', 'pipe', 'ignore'] })\n .toString()\n .split('\\n')\n .find((l) => l.startsWith('n'));\n return line ? fs.realpathSync(line.slice(1)) : null;\n } catch {\n return null; // pid gone, or we don't have permission to inspect it\n }\n};\n\n// The sweep below must never target this process's own wrappers: launched as\n// `npx qpq go:dev:api` (or via any shell whose -c string names qpq), the\n// ancestor chain (`sh -c \"npx ... qpq ...\"`, `npm exec qpq ...`) matches\n// isQpqCliCommand and shares our cwd, so excluding just process.pid isn't\n// enough — SIGINTing an ancestor tears down our own tree. Walk ppid to the\n// top and exclude the whole chain.\nconst getOwnAncestorPids = (): Set<number> => {\n const ancestors = new Set<number>();\n let pid = process.pid;\n while (pid > 1 && !ancestors.has(pid)) {\n ancestors.add(pid);\n try {\n pid = Number(\n execSync(`ps -o ppid= -p ${pid}`, { stdio: ['ignore', 'pipe', 'ignore'] })\n .toString()\n .trim(),\n );\n } catch {\n break;\n }\n }\n return ancestors;\n};\n\n// killStaleListeners only sees processes currently BOUND to one of the given\n// ports — it can't catch a still-running `qpq go:dev*` watcher whose spawned\n// dev-server child already died (a crash, or us killing just the child):\n// the watcher itself holds no port at that moment, so it's invisible to a\n// port scan, yet it's still alive and can spawn a fresh child — and bind a\n// port — moments later, racing whatever we're about to start. Close that gap\n// by finding any OTHER qpq CLI process rooted in the same repo checkout\n// (matched by cwd, not by port or command-line args, which vary by how it\n// was invoked) and killing it outright, regardless of what it currently has\n// bound. SIGINT (not SIGTERM) so goDevApiCommand/goDevWebCommand's own\n// handler runs and tears down its child cleanly rather than orphaning it.\nexport const killOtherQpqDevProcesses = (root: string): void => {\n const resolvedRoot = fs.realpathSync(root);\n const ownPids = getOwnAncestorPids();\n\n let lines: string[] = [];\n try {\n lines = execSync('ps -axo pid=,command=', { stdio: ['ignore', 'pipe', 'ignore'] })\n .toString()\n .split('\\n');\n } catch {\n return;\n }\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n const spaceIndex = trimmed.indexOf(' ');\n if (spaceIndex === -1) continue;\n\n const pid = Number(trimmed.slice(0, spaceIndex));\n const command = trimmed.slice(spaceIndex + 1).trim();\n\n if (!pid || ownPids.has(pid) || !isQpqCliCommand(command)) continue;\n if (getProcessCwd(pid) !== resolvedRoot) continue;\n\n console.log(`Killing other qpq dev process for this repo (pid ${pid}): ${command}`);\n process.kill(pid, 'SIGINT');\n waitForPidGone(pid);\n }\n};\n\nexport const killStaleListeners = (ports: number[], isOurs: (command: string) => boolean): void => {\n for (const port of ports) {\n let pids: number[] = [];\n try {\n pids = execSync(`lsof -t -iTCP:${port} -sTCP:LISTEN`, {\n stdio: ['ignore', 'pipe', 'ignore'],\n })\n .toString()\n .split('\\n')\n .map((line) => Number(line.trim()))\n .filter(Boolean);\n } catch {\n continue; // lsof exits non-zero when nothing is listening\n }\n\n for (const pid of pids) {\n let command = '';\n try {\n command = execSync(`ps -p ${pid} -o command=`).toString().trim();\n } catch {\n continue; // already gone\n }\n if (isOurs(command)) {\n console.log(`Killing stale dev server on port ${port} (pid ${pid})`);\n process.kill(pid);\n waitForPidGone(pid);\n } else {\n console.warn(`Port ${port} is in use by an unrelated process (pid ${pid}: ${command}) — not killing it.`);\n }\n }\n }\n};\n"]}
@@ -1,3 +1,13 @@
1
1
  export declare const promptSelect: (message: string, choices: string[], defaultChoice?: string) => Promise<string>;
2
+ export declare const promptSelectDetailed: <T>(message: string, choices: {
3
+ name: string;
4
+ value: T;
5
+ short?: string;
6
+ description?: string;
7
+ }[]) => Promise<T>;
2
8
  export declare const promptYesNo: (message: string) => Promise<boolean>;
3
9
  export declare const promptText: (message: string) => Promise<string>;
10
+ export declare const promptCheckbox: <T>(message: string, choices: {
11
+ name: string;
12
+ value: T;
13
+ }[]) => Promise<T[]>;
@@ -5,6 +5,13 @@ export const promptSelect = async (message, choices, defaultChoice) => {
5
5
  const { select } = await import('@inquirer/prompts');
6
6
  return select({ message, choices, default: defaultChoice });
7
7
  };
8
+ // A single-choice list prompt over labelled values; a choice's optional
9
+ // `short` replaces its name in the answer line once picked, and its optional
10
+ // `description` renders under the list while it is highlighted.
11
+ export const promptSelectDetailed = async (message, choices) => {
12
+ const { select } = await import('@inquirer/prompts');
13
+ return select({ message, choices });
14
+ };
8
15
  // A yes/no list prompt, returning true for "Yes".
9
16
  export const promptYesNo = async (message) => (await promptSelect(message, ['No', 'Yes'])) === 'Yes';
10
17
  // A free-text prompt — used where a y/n is too easy to fat-finger (destructive
@@ -13,4 +20,9 @@ export const promptText = async (message) => {
13
20
  const { input } = await import('@inquirer/prompts');
14
21
  return input({ message });
15
22
  };
23
+ // A multi-select checkbox prompt over labelled values.
24
+ export const promptCheckbox = async (message, choices) => {
25
+ const { checkbox } = await import('@inquirer/prompts');
26
+ return checkbox({ message, choices });
27
+ };
16
28
  //# sourceMappingURL=prompts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../../src/lib/prompts.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,wEAAwE;AACxE,sBAAsB;AACtB,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,OAAe,EAAE,OAAiB,EAAE,aAAsB,EAAmB,EAAE;IAChH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;AAC9D,CAAC,CAAC;AAEF,kDAAkD;AAClD,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAAE,OAAe,EAAoB,EAAE,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC;AAE/H,+EAA+E;AAC/E,2DAA2D;AAC3D,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,OAAe,EAAmB,EAAE;IACnE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACpD,OAAO,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAC5B,CAAC,CAAC","sourcesContent":["// Present a single-choice list prompt and return the selected value.\n// @inquirer/prompts is imported lazily so commands only pay for it when\n// actually prompting.\nexport const promptSelect = async (message: string, choices: string[], defaultChoice?: string): Promise<string> => {\n const { select } = await import('@inquirer/prompts');\n return select({ message, choices, default: defaultChoice });\n};\n\n// A yes/no list prompt, returning true for \"Yes\".\nexport const promptYesNo = async (message: string): Promise<boolean> => (await promptSelect(message, ['No', 'Yes'])) === 'Yes';\n\n// A free-text prompt — used where a y/n is too easy to fat-finger (destructive\n// confirmations that require typing the target name back).\nexport const promptText = async (message: string): Promise<string> => {\n const { input } = await import('@inquirer/prompts');\n return input({ message });\n};\n"]}
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../../src/lib/prompts.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,wEAAwE;AACxE,sBAAsB;AACtB,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,OAAe,EAAE,OAAiB,EAAE,aAAsB,EAAmB,EAAE;IAChH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;AAC9D,CAAC,CAAC;AAEF,wEAAwE;AACxE,6EAA6E;AAC7E,gEAAgE;AAChE,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EACvC,OAAe,EACf,OAA2E,EAC/D,EAAE;IACd,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AACtC,CAAC,CAAC;AAEF,kDAAkD;AAClD,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAAE,OAAe,EAAoB,EAAE,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC;AAE/H,+EAA+E;AAC/E,2DAA2D;AAC3D,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,OAAe,EAAmB,EAAE;IACnE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACpD,OAAO,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAC5B,CAAC,CAAC;AAEF,uDAAuD;AACvD,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAK,OAAe,EAAE,OAAqC,EAAgB,EAAE;IAC9G,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACvD,OAAO,QAAQ,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AACxC,CAAC,CAAC","sourcesContent":["// Present a single-choice list prompt and return the selected value.\n// @inquirer/prompts is imported lazily so commands only pay for it when\n// actually prompting.\nexport const promptSelect = async (message: string, choices: string[], defaultChoice?: string): Promise<string> => {\n const { select } = await import('@inquirer/prompts');\n return select({ message, choices, default: defaultChoice });\n};\n\n// A single-choice list prompt over labelled values; a choice's optional\n// `short` replaces its name in the answer line once picked, and its optional\n// `description` renders under the list while it is highlighted.\nexport const promptSelectDetailed = async <T>(\n message: string,\n choices: { name: string; value: T; short?: string; description?: string }[],\n): Promise<T> => {\n const { select } = await import('@inquirer/prompts');\n return select({ message, choices });\n};\n\n// A yes/no list prompt, returning true for \"Yes\".\nexport const promptYesNo = async (message: string): Promise<boolean> => (await promptSelect(message, ['No', 'Yes'])) === 'Yes';\n\n// A free-text prompt — used where a y/n is too easy to fat-finger (destructive\n// confirmations that require typing the target name back).\nexport const promptText = async (message: string): Promise<string> => {\n const { input } = await import('@inquirer/prompts');\n return input({ message });\n};\n\n// A multi-select checkbox prompt over labelled values.\nexport const promptCheckbox = async <T>(message: string, choices: { name: string; value: T }[]): Promise<T[]> => {\n const { checkbox } = await import('@inquirer/prompts');\n return checkbox({ message, choices });\n};\n"]}
@@ -1,3 +1,4 @@
1
1
  import { type QPQConfig } from 'quidproquo-core';
2
2
  export declare const loadServiceQpqConfig: (appName: string, service: string) => QPQConfig;
3
3
  export declare const getAppPrefix: (appName: string) => string;
4
+ export declare const getServiceNamesWithFederation: (appName: string, serviceNames: string[]) => string[];
@@ -11,4 +11,11 @@ export const getAppPrefix = (appName) => {
11
11
  throw new Error(`No services found for app ${appName}`);
12
12
  return qpqCoreUtils.getApplicationName(loadServiceQpqConfig(appName, services[0]));
13
13
  };
14
+ // The subset of the given services that opt into module-federation publishing,
15
+ // i.e. declare defineFederatedModuleStore(...). Federation is a config setting,
16
+ // not a directory convention (unlike views/ backends), so detecting it means
17
+ // loading each service's config — which is why this lives here and not in the
18
+ // directory-only discovery module. Used by the deploy drivers to decide which
19
+ // backends to publish federated remotes for after a deploy.
20
+ export const getServiceNamesWithFederation = (appName, serviceNames) => serviceNames.filter((service) => !!qpqCoreUtils.getFederatedModuleStore(loadServiceQpqConfig(appName, service)));
14
21
  //# sourceMappingURL=qpqConfigs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"qpqConfigs.js","sourceRoot":"","sources":["../../../src/lib/qpqConfigs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAExE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEvD,wEAAwE;AACxE,2EAA2E;AAC3E,uBAAuB;AACvB,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,OAAe,EAAE,OAAe,EAAa,EAAE,CAAC,4BAA4B,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAE/I,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAe,EAAU,EAAE;IACtD,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IAC1C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,OAAO,EAAE,CAAC,CAAC;IACnF,OAAO,YAAY,CAAC,kBAAkB,CAAC,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrF,CAAC,CAAC","sourcesContent":["import { type QPQConfig, qpqCoreUtils } from 'quidproquo-core';\nimport { getWorkspaceServiceQpqConfig } from 'quidproquo-deploy-awscdk';\n\nimport { getRoot, getServiceNames } from './discovery';\n\n// Loads a service's live infrastructure.ts (qpq runs with ts-node hooks\n// registered, so requiring TS works and `@scope/*` imports resolve through\n// workspace symlinks).\nexport const loadServiceQpqConfig = (appName: string, service: string): QPQConfig => getWorkspaceServiceQpqConfig(getRoot(), appName, service);\n\nexport const getAppPrefix = (appName: string): string => {\n const services = getServiceNames(appName);\n if (services.length === 0) throw new Error(`No services found for app ${appName}`);\n return qpqCoreUtils.getApplicationName(loadServiceQpqConfig(appName, services[0]));\n};\n"]}
1
+ {"version":3,"file":"qpqConfigs.js","sourceRoot":"","sources":["../../../src/lib/qpqConfigs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAExE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEvD,wEAAwE;AACxE,2EAA2E;AAC3E,uBAAuB;AACvB,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,OAAe,EAAE,OAAe,EAAa,EAAE,CAAC,4BAA4B,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAE/I,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAe,EAAU,EAAE;IACtD,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IAC1C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,OAAO,EAAE,CAAC,CAAC;IACnF,OAAO,YAAY,CAAC,kBAAkB,CAAC,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrF,CAAC,CAAC;AAEF,+EAA+E;AAC/E,gFAAgF;AAChF,6EAA6E;AAC7E,8EAA8E;AAC9E,8EAA8E;AAC9E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,OAAe,EAAE,YAAsB,EAAY,EAAE,CACjG,YAAY,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,uBAAuB,CAAC,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC","sourcesContent":["import { type QPQConfig, qpqCoreUtils } from 'quidproquo-core';\nimport { getWorkspaceServiceQpqConfig } from 'quidproquo-deploy-awscdk';\n\nimport { getRoot, getServiceNames } from './discovery';\n\n// Loads a service's live infrastructure.ts (qpq runs with ts-node hooks\n// registered, so requiring TS works and `@scope/*` imports resolve through\n// workspace symlinks).\nexport const loadServiceQpqConfig = (appName: string, service: string): QPQConfig => getWorkspaceServiceQpqConfig(getRoot(), appName, service);\n\nexport const getAppPrefix = (appName: string): string => {\n const services = getServiceNames(appName);\n if (services.length === 0) throw new Error(`No services found for app ${appName}`);\n return qpqCoreUtils.getApplicationName(loadServiceQpqConfig(appName, services[0]));\n};\n\n// The subset of the given services that opt into module-federation publishing,\n// i.e. declare defineFederatedModuleStore(...). Federation is a config setting,\n// not a directory convention (unlike views/ backends), so detecting it means\n// loading each service's config — which is why this lives here and not in the\n// directory-only discovery module. Used by the deploy drivers to decide which\n// backends to publish federated remotes for after a deploy.\nexport const getServiceNamesWithFederation = (appName: string, serviceNames: string[]): string[] =>\n serviceNames.filter((service) => !!qpqCoreUtils.getFederatedModuleStore(loadServiceQpqConfig(appName, service)));\n"]}
@@ -1,5 +1,5 @@
1
1
  // One app-selection contract for every app-facing command (prep / synth / go /
2
- // go:dev / go:dev:web / publish). Resolution order:
2
+ // go:dev / go:dev:api / go:dev:web / publish). Resolution order:
3
3
  // 1. --app <name> / --app=<name> on argv
4
4
  // 2. npm_config_app (npm run x --app foo, no --)
5
5
  // 3. an env var the command already honours (QPQ_DEV_APP / DEPLOY_APP_NAME)
@@ -1 +1 @@
1
- {"version":3,"file":"resolveAppSelection.js","sourceRoot":"","sources":["../../../src/lib/resolveAppSelection.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,oDAAoD;AACpD,2CAA2C;AAC3C,+EAA+E;AAC/E,kFAAkF;AAClF,kDAAkD;AAClD,qCAAqC;AACrC,6DAA6D;AAC7D,6EAA6E;AAC7E,+EAA+E;AAC/E,+CAA+C;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAUzC,MAAM,eAAe,GAAG,CAAC,SAA6B,EAAE,aAAuB,EAAS,EAAE;IACxF,IAAI,SAAS;QAAE,OAAO,CAAC,KAAK,CAAC,gBAAgB,SAAS,IAAI,CAAC,CAAC;IAC5D,OAAO,CAAC,KAAK,CAAC,eAAe,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzD,OAAO,CAAC,KAAK,CAAC,wDAAwD,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EAAE,OAA0B,EAAmB,EAAE;IACvF,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAC;IACzC,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,SAAS,GACb,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAEjI,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,OAAO,CAAC,QAAQ,IAAI,SAAS,KAAK,KAAK;YAAE,OAAO,KAAK,CAAC;QAC1D,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,eAAe,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QAClF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,cAAc,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC9C,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACxB,OAAO,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAClG,CAAC;IAED,OAAO,eAAe,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;AACnD,CAAC,CAAC","sourcesContent":["// One app-selection contract for every app-facing command (prep / synth / go /\n// go:dev / go:dev:web / publish). Resolution order:\n// 1. --app <name> / --app=<name> on argv\n// 2. npm_config_app (npm run x --app foo, no --)\n// 3. an env var the command already honours (QPQ_DEV_APP / DEPLOY_APP_NAME)\n// 4. exactly one app under apps/ -> auto-select\n// 5. interactive prompt (TTY only)\n// 6. otherwise: print the valid app list and exit non-zero\n// An unknown name (from any source) also prints the list and exits non-zero.\n// Commands that can meaningfully run for every app may pass allowAll to accept\n// `--app all` (returned as the literal 'all').\nimport { getArgValue } from './args';\nimport { getAvailableApps } from './discovery';\nimport { promptSelect } from './prompts';\n\nexport type ResolveAppOptions = {\n argv: string[];\n // Env var this command historically honours (e.g. QPQ_DEV_APP, DEPLOY_APP_NAME).\n envVar?: string;\n // Accept `--app all` and return 'all'.\n allowAll?: boolean;\n};\n\nconst exitWithAppList = (requested: string | undefined, availableApps: string[]): never => {\n if (requested) console.error(`Unknown app '${requested}'.`);\n console.error(`Valid apps: ${availableApps.join(', ')}`);\n console.error(`Pass one with --app <name> (e.g. qpq <command> --app ${availableApps[0]}).`);\n process.exit(1);\n};\n\nexport const resolveAppSelection = async (options: ResolveAppOptions): Promise<string> => {\n const availableApps = getAvailableApps();\n if (availableApps.length === 0) {\n console.error('No apps found under apps/.');\n process.exit(1);\n }\n\n const requested =\n getArgValue(options.argv, '--app') || process.env.npm_config_app || (options.envVar ? process.env[options.envVar] : undefined);\n\n if (requested) {\n if (options.allowAll && requested === 'all') return 'all';\n if (!availableApps.includes(requested)) exitWithAppList(requested, availableApps);\n return requested;\n }\n\n if (availableApps.length === 1) {\n console.log(`Using app: ${availableApps[0]}`);\n return availableApps[0];\n }\n\n if (process.stdin.isTTY) {\n return promptSelect('Select app', options.allowAll ? [...availableApps, 'all'] : availableApps);\n }\n\n return exitWithAppList(undefined, availableApps);\n};\n"]}
1
+ {"version":3,"file":"resolveAppSelection.js","sourceRoot":"","sources":["../../../src/lib/resolveAppSelection.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,iEAAiE;AACjE,2CAA2C;AAC3C,+EAA+E;AAC/E,kFAAkF;AAClF,kDAAkD;AAClD,qCAAqC;AACrC,6DAA6D;AAC7D,6EAA6E;AAC7E,+EAA+E;AAC/E,+CAA+C;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAUzC,MAAM,eAAe,GAAG,CAAC,SAA6B,EAAE,aAAuB,EAAS,EAAE;IACxF,IAAI,SAAS;QAAE,OAAO,CAAC,KAAK,CAAC,gBAAgB,SAAS,IAAI,CAAC,CAAC;IAC5D,OAAO,CAAC,KAAK,CAAC,eAAe,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzD,OAAO,CAAC,KAAK,CAAC,wDAAwD,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EAAE,OAA0B,EAAmB,EAAE;IACvF,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAC;IACzC,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,SAAS,GACb,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAEjI,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,OAAO,CAAC,QAAQ,IAAI,SAAS,KAAK,KAAK;YAAE,OAAO,KAAK,CAAC;QAC1D,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,eAAe,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QAClF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,cAAc,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC9C,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACxB,OAAO,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAClG,CAAC;IAED,OAAO,eAAe,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;AACnD,CAAC,CAAC","sourcesContent":["// One app-selection contract for every app-facing command (prep / synth / go /\n// go:dev / go:dev:api / go:dev:web / publish). Resolution order:\n// 1. --app <name> / --app=<name> on argv\n// 2. npm_config_app (npm run x --app foo, no --)\n// 3. an env var the command already honours (QPQ_DEV_APP / DEPLOY_APP_NAME)\n// 4. exactly one app under apps/ -> auto-select\n// 5. interactive prompt (TTY only)\n// 6. otherwise: print the valid app list and exit non-zero\n// An unknown name (from any source) also prints the list and exits non-zero.\n// Commands that can meaningfully run for every app may pass allowAll to accept\n// `--app all` (returned as the literal 'all').\nimport { getArgValue } from './args';\nimport { getAvailableApps } from './discovery';\nimport { promptSelect } from './prompts';\n\nexport type ResolveAppOptions = {\n argv: string[];\n // Env var this command historically honours (e.g. QPQ_DEV_APP, DEPLOY_APP_NAME).\n envVar?: string;\n // Accept `--app all` and return 'all'.\n allowAll?: boolean;\n};\n\nconst exitWithAppList = (requested: string | undefined, availableApps: string[]): never => {\n if (requested) console.error(`Unknown app '${requested}'.`);\n console.error(`Valid apps: ${availableApps.join(', ')}`);\n console.error(`Pass one with --app <name> (e.g. qpq <command> --app ${availableApps[0]}).`);\n process.exit(1);\n};\n\nexport const resolveAppSelection = async (options: ResolveAppOptions): Promise<string> => {\n const availableApps = getAvailableApps();\n if (availableApps.length === 0) {\n console.error('No apps found under apps/.');\n process.exit(1);\n }\n\n const requested =\n getArgValue(options.argv, '--app') || process.env.npm_config_app || (options.envVar ? process.env[options.envVar] : undefined);\n\n if (requested) {\n if (options.allowAll && requested === 'all') return 'all';\n if (!availableApps.includes(requested)) exitWithAppList(requested, availableApps);\n return requested;\n }\n\n if (availableApps.length === 1) {\n console.log(`Using app: ${availableApps[0]}`);\n return availableApps[0];\n }\n\n if (process.stdin.isTTY) {\n return promptSelect('Select app', options.allowAll ? [...availableApps, 'all'] : availableApps);\n }\n\n return exitWithAppList(undefined, availableApps);\n};\n"]}
@@ -7,4 +7,9 @@ export type TaskFailure = {
7
7
  error: Error;
8
8
  };
9
9
  export declare const runTasks: (tasks: Task[], limit: number) => Promise<TaskFailure[]>;
10
+ export type DependencyTask = Task & {
11
+ id: string;
12
+ deps: string[];
13
+ };
14
+ export declare const runDependencyTasks: (tasks: DependencyTask[], limit: number) => Promise<TaskFailure[]>;
10
15
  export declare const assertNoFailures: (phaseName: string, failures: TaskFailure[]) => void;
@@ -18,6 +18,86 @@ export const runTasks = async (tasks, limit) => {
18
18
  }));
19
19
  return failures;
20
20
  };
21
+ // Run tasks with at most `limit` in flight, but a task only starts once every
22
+ // task in its `deps` has succeeded. A failure doesn't stop unrelated tasks;
23
+ // dependents of the failed task are skipped (logged, not returned as failures,
24
+ // the root cause already is). Never rejects. Tasks left unstartable by a
25
+ // dependency cycle come back as failures.
26
+ export const runDependencyTasks = async (tasks, limit) => {
27
+ const failures = [];
28
+ const succeeded = new Set();
29
+ const failed = new Set();
30
+ const started = new Set();
31
+ const pipelines = [];
32
+ let slots = Math.max(1, limit);
33
+ const waiting = [];
34
+ const acquire = async () => {
35
+ if (slots > 0) {
36
+ slots -= 1;
37
+ return;
38
+ }
39
+ await new Promise((resolve) => waiting.push(resolve));
40
+ };
41
+ const release = () => {
42
+ const next = waiting.shift();
43
+ if (next) {
44
+ next();
45
+ }
46
+ else {
47
+ slots += 1;
48
+ }
49
+ };
50
+ const runOne = async (task) => {
51
+ await acquire();
52
+ try {
53
+ await task.run();
54
+ succeeded.add(task.id);
55
+ }
56
+ catch (error) {
57
+ failed.add(task.id);
58
+ failures.push({ label: task.label, error: error });
59
+ }
60
+ finally {
61
+ release();
62
+ }
63
+ launchReady();
64
+ };
65
+ // Repeatedly sweep until stable: launching one task or cascading one skip can
66
+ // unblock (or block) tasks earlier in the list.
67
+ const launchReady = () => {
68
+ let changed = true;
69
+ while (changed) {
70
+ changed = false;
71
+ for (const task of tasks) {
72
+ if (started.has(task.id))
73
+ continue;
74
+ if (task.deps.some((dep) => failed.has(dep))) {
75
+ started.add(task.id);
76
+ failed.add(task.id); // cascade so transitive dependents skip too
77
+ console.log(`Skipping ${task.label} (a dependency failed)`);
78
+ changed = true;
79
+ }
80
+ else if (task.deps.every((dep) => succeeded.has(dep))) {
81
+ started.add(task.id);
82
+ pipelines.push(runOne(task));
83
+ changed = true;
84
+ }
85
+ }
86
+ }
87
+ };
88
+ launchReady();
89
+ // runOne re-invokes launchReady as tasks finish, pushing newly unblocked
90
+ // pipelines while earlier ones are being awaited; drain until it stays empty.
91
+ while (pipelines.length > 0) {
92
+ await Promise.all(pipelines.splice(0, pipelines.length));
93
+ }
94
+ for (const task of tasks) {
95
+ if (!started.has(task.id)) {
96
+ failures.push({ label: task.label, error: new Error('unstartable: dependency cycle') });
97
+ }
98
+ }
99
+ return failures;
100
+ };
21
101
  export const assertNoFailures = (phaseName, failures) => {
22
102
  if (failures.length === 0)
23
103
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"runTasks.js","sourceRoot":"","sources":["../../../src/lib/runTasks.ts"],"names":[],"mappings":"AAGA,+EAA+E;AAC/E,mEAAmE;AACnE,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAAE,KAAa,EAAE,KAAa,EAA0B,EAAE;IACrF,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,MAAM,OAAO,CAAC,GAAG,CACf,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,KAAK,IAAI,EAAE;QAC7C,OAAO,SAAS,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;YAC9B,SAAS,IAAI,CAAC,CAAC;YACf,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;YACnB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,KAAc,EAAE,CAAC,CAAC;YAC9D,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAiB,EAAE,QAAuB,EAAQ,EAAE;IACnF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAClC,OAAO,CAAC,KAAK,CAAC,KAAK,SAAS,cAAc,CAAC,CAAC;IAC5C,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC7F,MAAM,IAAI,KAAK,CAAC,GAAG,SAAS,KAAK,QAAQ,CAAC,MAAM,iBAAiB,CAAC,CAAC;AACrE,CAAC,CAAC","sourcesContent":["export type Task = { label: string; run: () => Promise<void> };\nexport type TaskFailure = { label: string; error: Error };\n\n// Run tasks with at most `limit` in flight; never rejects — failures come back\n// labelled so a whole wave can finish before we report what broke.\nexport const runTasks = async (tasks: Task[], limit: number): Promise<TaskFailure[]> => {\n const failures: TaskFailure[] = [];\n let nextIndex = 0;\n\n const workerCount = Math.max(1, Math.min(limit, tasks.length));\n await Promise.all(\n Array.from({ length: workerCount }, async () => {\n while (nextIndex < tasks.length) {\n const task = tasks[nextIndex];\n nextIndex += 1;\n try {\n await task.run();\n } catch (error) {\n failures.push({ label: task.label, error: error as Error });\n }\n }\n }),\n );\n\n return failures;\n};\n\nexport const assertNoFailures = (phaseName: string, failures: TaskFailure[]): void => {\n if (failures.length === 0) return;\n console.error(`\\n${phaseName} failed for:`);\n failures.forEach((failure) => console.error(` ${failure.label}: ${failure.error.message}`));\n throw new Error(`${phaseName}: ${failures.length} task(s) failed`);\n};\n"]}
1
+ {"version":3,"file":"runTasks.js","sourceRoot":"","sources":["../../../src/lib/runTasks.ts"],"names":[],"mappings":"AAGA,+EAA+E;AAC/E,mEAAmE;AACnE,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAAE,KAAa,EAAE,KAAa,EAA0B,EAAE;IACrF,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,MAAM,OAAO,CAAC,GAAG,CACf,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,KAAK,IAAI,EAAE;QAC7C,OAAO,SAAS,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;YAC9B,SAAS,IAAI,CAAC,CAAC;YACf,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;YACnB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,KAAc,EAAE,CAAC,CAAC;YAC9D,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAUF,8EAA8E;AAC9E,4EAA4E;AAC5E,+EAA+E;AAC/E,yEAAyE;AACzE,0CAA0C;AAC1C,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EAAE,KAAuB,EAAE,KAAa,EAA0B,EAAE;IACzG,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,SAAS,GAAyB,EAAE,CAAC;IAE3C,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAC/B,MAAM,OAAO,GAAsB,EAAE,CAAC;IAEtC,MAAM,OAAO,GAAG,KAAK,IAAmB,EAAE;QACxC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,KAAK,IAAI,CAAC,CAAC;YACX,OAAO;QACT,CAAC;QACD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9D,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,EAAE,CAAC;QACT,CAAC;aAAM,CAAC;YACN,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,KAAK,EAAE,IAAoB,EAAiB,EAAE;QAC3D,MAAM,OAAO,EAAE,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;YACjB,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,KAAc,EAAE,CAAC,CAAC;QAC9D,CAAC;gBAAS,CAAC;YACT,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,WAAW,EAAE,CAAC;IAChB,CAAC,CAAC;IAEF,8EAA8E;IAC9E,gDAAgD;IAChD,MAAM,WAAW,GAAG,GAAS,EAAE;QAC7B,IAAI,OAAO,GAAG,IAAI,CAAC;QACnB,OAAO,OAAO,EAAE,CAAC;YACf,OAAO,GAAG,KAAK,CAAC;YAChB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;oBAAE,SAAS;gBACnC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;oBAC7C,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBACrB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,4CAA4C;oBACjE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,KAAK,wBAAwB,CAAC,CAAC;oBAC5D,OAAO,GAAG,IAAI,CAAC;gBACjB,CAAC;qBAAM,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;oBACxD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBACrB,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;oBAC7B,OAAO,GAAG,IAAI,CAAC;gBACjB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,WAAW,EAAE,CAAC;IACd,yEAAyE;IACzE,8EAA8E;IAC9E,OAAO,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,+BAA+B,CAAC,EAAE,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAiB,EAAE,QAAuB,EAAQ,EAAE;IACnF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAClC,OAAO,CAAC,KAAK,CAAC,KAAK,SAAS,cAAc,CAAC,CAAC;IAC5C,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC7F,MAAM,IAAI,KAAK,CAAC,GAAG,SAAS,KAAK,QAAQ,CAAC,MAAM,iBAAiB,CAAC,CAAC;AACrE,CAAC,CAAC","sourcesContent":["export type Task = { label: string; run: () => Promise<void> };\nexport type TaskFailure = { label: string; error: Error };\n\n// Run tasks with at most `limit` in flight; never rejects — failures come back\n// labelled so a whole wave can finish before we report what broke.\nexport const runTasks = async (tasks: Task[], limit: number): Promise<TaskFailure[]> => {\n const failures: TaskFailure[] = [];\n let nextIndex = 0;\n\n const workerCount = Math.max(1, Math.min(limit, tasks.length));\n await Promise.all(\n Array.from({ length: workerCount }, async () => {\n while (nextIndex < tasks.length) {\n const task = tasks[nextIndex];\n nextIndex += 1;\n try {\n await task.run();\n } catch (error) {\n failures.push({ label: task.label, error: error as Error });\n }\n }\n }),\n );\n\n return failures;\n};\n\nexport type DependencyTask = Task & {\n // Unique id other tasks reference in `deps` (e.g. a package name).\n id: string;\n // Ids of tasks that must succeed before this one starts. Ids not present in\n // the task list should be filtered out by the caller.\n deps: string[];\n};\n\n// Run tasks with at most `limit` in flight, but a task only starts once every\n// task in its `deps` has succeeded. A failure doesn't stop unrelated tasks;\n// dependents of the failed task are skipped (logged, not returned as failures,\n// the root cause already is). Never rejects. Tasks left unstartable by a\n// dependency cycle come back as failures.\nexport const runDependencyTasks = async (tasks: DependencyTask[], limit: number): Promise<TaskFailure[]> => {\n const failures: TaskFailure[] = [];\n const succeeded = new Set<string>();\n const failed = new Set<string>();\n const started = new Set<string>();\n const pipelines: Array<Promise<void>> = [];\n\n let slots = Math.max(1, limit);\n const waiting: Array<() => void> = [];\n\n const acquire = async (): Promise<void> => {\n if (slots > 0) {\n slots -= 1;\n return;\n }\n await new Promise<void>((resolve) => waiting.push(resolve));\n };\n\n const release = (): void => {\n const next = waiting.shift();\n if (next) {\n next();\n } else {\n slots += 1;\n }\n };\n\n const runOne = async (task: DependencyTask): Promise<void> => {\n await acquire();\n try {\n await task.run();\n succeeded.add(task.id);\n } catch (error) {\n failed.add(task.id);\n failures.push({ label: task.label, error: error as Error });\n } finally {\n release();\n }\n launchReady();\n };\n\n // Repeatedly sweep until stable: launching one task or cascading one skip can\n // unblock (or block) tasks earlier in the list.\n const launchReady = (): void => {\n let changed = true;\n while (changed) {\n changed = false;\n for (const task of tasks) {\n if (started.has(task.id)) continue;\n if (task.deps.some((dep) => failed.has(dep))) {\n started.add(task.id);\n failed.add(task.id); // cascade so transitive dependents skip too\n console.log(`Skipping ${task.label} (a dependency failed)`);\n changed = true;\n } else if (task.deps.every((dep) => succeeded.has(dep))) {\n started.add(task.id);\n pipelines.push(runOne(task));\n changed = true;\n }\n }\n }\n };\n\n launchReady();\n // runOne re-invokes launchReady as tasks finish, pushing newly unblocked\n // pipelines while earlier ones are being awaited; drain until it stays empty.\n while (pipelines.length > 0) {\n await Promise.all(pipelines.splice(0, pipelines.length));\n }\n\n for (const task of tasks) {\n if (!started.has(task.id)) {\n failures.push({ label: task.label, error: new Error('unstartable: dependency cycle') });\n }\n }\n\n return failures;\n};\n\nexport const assertNoFailures = (phaseName: string, failures: TaskFailure[]): void => {\n if (failures.length === 0) return;\n console.error(`\\n${phaseName} failed for:`);\n failures.forEach((failure) => console.error(` ${failure.label}: ${failure.error.message}`));\n throw new Error(`${phaseName}: ${failures.length} task(s) failed`);\n};\n"]}
@@ -0,0 +1,2 @@
1
+ import { ChildProcess } from 'child_process';
2
+ export declare const startTypeWatcher: (root: string) => ChildProcess | null;