quidproquo-cli 0.1.9 → 0.1.11

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 (37) hide show
  1. package/lib/commonjs/cli/commandRegistry.js +5 -0
  2. package/lib/commonjs/cli/commandRegistry.js.map +1 -1
  3. package/lib/commonjs/commands/checkCircular.d.ts +1 -0
  4. package/lib/commonjs/commands/checkCircular.js +70 -0
  5. package/lib/commonjs/commands/checkCircular.js.map +1 -0
  6. package/lib/commonjs/commands/synth.js +12 -1
  7. package/lib/commonjs/commands/synth.js.map +1 -1
  8. package/lib/commonjs/lib/circularImports.d.ts +3 -0
  9. package/lib/commonjs/lib/circularImports.js +79 -0
  10. package/lib/commonjs/lib/circularImports.js.map +1 -0
  11. package/lib/commonjs/lib/runCommand.d.ts +1 -0
  12. package/lib/commonjs/lib/runCommand.js +28 -3
  13. package/lib/commonjs/lib/runCommand.js.map +1 -1
  14. package/lib/commonjs/platforms/aws/goDocker.js +37 -35
  15. package/lib/commonjs/platforms/aws/goDocker.js.map +1 -1
  16. package/lib/commonjs/platforms/aws/viewsSync.d.ts +1 -0
  17. package/lib/commonjs/platforms/aws/viewsSync.js +31 -2
  18. package/lib/commonjs/platforms/aws/viewsSync.js.map +1 -1
  19. package/lib/esm/cli/commandRegistry.js +5 -0
  20. package/lib/esm/cli/commandRegistry.js.map +1 -1
  21. package/lib/esm/commands/checkCircular.d.ts +1 -0
  22. package/lib/esm/commands/checkCircular.js +55 -0
  23. package/lib/esm/commands/checkCircular.js.map +1 -0
  24. package/lib/esm/commands/synth.js +12 -1
  25. package/lib/esm/commands/synth.js.map +1 -1
  26. package/lib/esm/lib/circularImports.d.ts +3 -0
  27. package/lib/esm/lib/circularImports.js +61 -0
  28. package/lib/esm/lib/circularImports.js.map +1 -0
  29. package/lib/esm/lib/runCommand.d.ts +1 -0
  30. package/lib/esm/lib/runCommand.js +25 -2
  31. package/lib/esm/lib/runCommand.js.map +1 -1
  32. package/lib/esm/platforms/aws/goDocker.js +38 -36
  33. package/lib/esm/platforms/aws/goDocker.js.map +1 -1
  34. package/lib/esm/platforms/aws/viewsSync.d.ts +1 -0
  35. package/lib/esm/platforms/aws/viewsSync.js +27 -1
  36. package/lib/esm/platforms/aws/viewsSync.js.map +1 -1
  37. package/package.json +15 -14
@@ -98,6 +98,11 @@ exports.cliCommands = [
98
98
  summary: 'Flip manifests to the uploaded versions',
99
99
  run: (a) => require('../commands/publish').publishDeployCommand(a),
100
100
  },
101
+ {
102
+ name: 'check:circular',
103
+ summary: 'Scan workspace sources for circular imports (fails on cycles; --warn to only report)',
104
+ run: (a) => require('../commands/checkCircular').checkCircularCommand(a),
105
+ },
101
106
  {
102
107
  name: 'hooks',
103
108
  usageArgs: '<name>',
@@ -1 +1 @@
1
- {"version":3,"file":"commandRegistry.js","sourceRoot":"","sources":["../../../src/cli/commandRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["// Single source of truth for every qpq command: its name, the one-line\n// summary (shared by `qpq help` and the interactive menu) and a runner.\n//\n// Command modules are required lazily inside each runner so a simple\n// `qpq help` doesn't pay for the whole toolchain, and so ts-node can be\n// registered before any command module (or anything it requires) loads.\nimport { promptText } from '../lib/prompts';\n\nexport type CliCommand = {\n name: string;\n // Argument hint shown in the usage text, e.g. '[svc] [stack]'.\n usageArgs?: string;\n summary: string;\n // Extra usage-only detail lines, indented under the summary.\n usageExtra?: string[];\n // Menu-only: commands that need a positional argument (which the menu has\n // no argv to carry) gather it here before running.\n promptArgs?: () => Promise<string[]>;\n run: (commandArgv: string[]) => Promise<void>;\n};\n\n// `qpq hooks` needs a hook name; when launched from the menu there is no\n// argv to pass it in, so ask.\nconst promptHookName = async (): Promise<string[]> => [\n await promptText('Hook name (runs qpq:<name>, or plain <name>, in every app/package that defines it):'),\n];\n\n/* eslint-disable @typescript-eslint/no-require-imports */\nexport const cliCommands: CliCommand[] = [\n {\n name: 'go',\n usageArgs: '[svc] [stack]',\n summary: 'Deploy services (interactive; platform from deploy.config.json, default aws).',\n usageExtra: [\n 'Pass positional args to skip prompts, e.g. qpq go all all — svc is',\n \"'all' | comma-list | account | domain | bootstrap; stack is all|inf|api|web|views.\",\n \"A full 'all all' (or 'all api') deploy also publishes federated backends.\",\n ],\n run: (a) => require('../commands/go').goCommand(a),\n },\n {\n name: 'go:docker',\n summary: 'Same as go (incl. positional args), but deploys in parallel via docker',\n run: (a) => require('../commands/goDocker').goDockerCommand(a),\n },\n {\n name: 'go:dev',\n summary: 'Run the full local dev stack (api + web) in one process',\n run: (a) => require('../commands/goDev').goDevCommand(a),\n },\n {\n name: 'go:dev:api',\n summary: 'Run the local API dev server (hot reload)',\n run: (a) => require('../commands/goDevApi').goDevApiCommand(a),\n },\n {\n name: 'go:dev:web',\n summary: 'Run every views microfrontend dev server',\n run: (a) => require('../commands/goDevWeb').goDevWebCommand(a),\n },\n {\n name: 'teardown',\n summary: 'Destroy web/api/inf stacks for selected services (interactive)',\n run: (a) => require('../commands/teardown').teardownCommand(a),\n },\n {\n name: 'clear-resources',\n summary: 'Empty selected buckets/tables (data only, stacks untouched; interactive)',\n run: (a) => require('../commands/clearResources').clearResourcesCommand(a),\n },\n {\n name: 'synth',\n usageArgs: '[service]',\n summary: 'Synth QPQ configs to dist/apps/<app>/infrastructure',\n run: (a) => require('../commands/synth').synthCommand(a),\n },\n {\n name: 'prep',\n summary: 'Regenerate apps/<app>/tsconfig.federated.json',\n run: (a) => require('../commands/prep').prepCommand(a),\n },\n {\n name: 'publish',\n summary: 'Build + upload + deploy federated remotes',\n run: (a) => require('../commands/publish').publishCommand(a),\n },\n {\n name: 'publish:build',\n summary: 'Build federated remotes locally',\n run: (a) => require('../commands/publish').publishBuildCommand(a),\n },\n {\n name: 'publish:upload',\n summary: 'Upload built version dirs (nothing goes live)',\n run: (a) => require('../commands/publish').publishUploadCommand(a),\n },\n {\n name: 'publish:deploy',\n summary: 'Flip manifests to the uploaded versions',\n run: (a) => require('../commands/publish').publishDeployCommand(a),\n },\n {\n name: 'hooks',\n usageArgs: '<name>',\n summary: 'Run qpq:<name> (or plain <name>) in every app/package that defines it (dependency ordered, parallel; --jobs=N)',\n promptArgs: promptHookName,\n run: (a) => require('../commands/hooks').hooksCommand(a),\n },\n];\n/* eslint-enable @typescript-eslint/no-require-imports */\n\n// Width of the \"name [args]\" column in the usage text.\nconst usageColumnWidth = 17;\n\nexport const buildUsage = (): string => {\n const commandLines = cliCommands.flatMap((command) => {\n const nameAndArgs = command.usageArgs ? `${command.name} ${command.usageArgs}` : command.name;\n const extraLines = (command.usageExtra ?? []).map((line) => ` ${''.padEnd(usageColumnWidth)}${line}`);\n return [` ${nameAndArgs.padEnd(usageColumnWidth)}${command.summary}`, ...extraLines];\n });\n\n return [\n 'qpq — build, dev and deploy orchestration for QPQ apps',\n '',\n 'Usage: qpq <command> [--app <name>] [--env <name>] [--platform <name>]',\n ' qpq (no command opens an interactive menu)',\n '',\n 'Commands:',\n ...commandLines,\n '',\n ].join('\\n');\n};\n"]}
1
+ {"version":3,"file":"commandRegistry.js","sourceRoot":"","sources":["../../../src/cli/commandRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["// Single source of truth for every qpq command: its name, the one-line\n// summary (shared by `qpq help` and the interactive menu) and a runner.\n//\n// Command modules are required lazily inside each runner so a simple\n// `qpq help` doesn't pay for the whole toolchain, and so ts-node can be\n// registered before any command module (or anything it requires) loads.\nimport { promptText } from '../lib/prompts';\n\nexport type CliCommand = {\n name: string;\n // Argument hint shown in the usage text, e.g. '[svc] [stack]'.\n usageArgs?: string;\n summary: string;\n // Extra usage-only detail lines, indented under the summary.\n usageExtra?: string[];\n // Menu-only: commands that need a positional argument (which the menu has\n // no argv to carry) gather it here before running.\n promptArgs?: () => Promise<string[]>;\n run: (commandArgv: string[]) => Promise<void>;\n};\n\n// `qpq hooks` needs a hook name; when launched from the menu there is no\n// argv to pass it in, so ask.\nconst promptHookName = async (): Promise<string[]> => [\n await promptText('Hook name (runs qpq:<name>, or plain <name>, in every app/package that defines it):'),\n];\n\n/* eslint-disable @typescript-eslint/no-require-imports */\nexport const cliCommands: CliCommand[] = [\n {\n name: 'go',\n usageArgs: '[svc] [stack]',\n summary: 'Deploy services (interactive; platform from deploy.config.json, default aws).',\n usageExtra: [\n 'Pass positional args to skip prompts, e.g. qpq go all all — svc is',\n \"'all' | comma-list | account | domain | bootstrap; stack is all|inf|api|web|views.\",\n \"A full 'all all' (or 'all api') deploy also publishes federated backends.\",\n ],\n run: (a) => require('../commands/go').goCommand(a),\n },\n {\n name: 'go:docker',\n summary: 'Same as go (incl. positional args), but deploys in parallel via docker',\n run: (a) => require('../commands/goDocker').goDockerCommand(a),\n },\n {\n name: 'go:dev',\n summary: 'Run the full local dev stack (api + web) in one process',\n run: (a) => require('../commands/goDev').goDevCommand(a),\n },\n {\n name: 'go:dev:api',\n summary: 'Run the local API dev server (hot reload)',\n run: (a) => require('../commands/goDevApi').goDevApiCommand(a),\n },\n {\n name: 'go:dev:web',\n summary: 'Run every views microfrontend dev server',\n run: (a) => require('../commands/goDevWeb').goDevWebCommand(a),\n },\n {\n name: 'teardown',\n summary: 'Destroy web/api/inf stacks for selected services (interactive)',\n run: (a) => require('../commands/teardown').teardownCommand(a),\n },\n {\n name: 'clear-resources',\n summary: 'Empty selected buckets/tables (data only, stacks untouched; interactive)',\n run: (a) => require('../commands/clearResources').clearResourcesCommand(a),\n },\n {\n name: 'synth',\n usageArgs: '[service]',\n summary: 'Synth QPQ configs to dist/apps/<app>/infrastructure',\n run: (a) => require('../commands/synth').synthCommand(a),\n },\n {\n name: 'prep',\n summary: 'Regenerate apps/<app>/tsconfig.federated.json',\n run: (a) => require('../commands/prep').prepCommand(a),\n },\n {\n name: 'publish',\n summary: 'Build + upload + deploy federated remotes',\n run: (a) => require('../commands/publish').publishCommand(a),\n },\n {\n name: 'publish:build',\n summary: 'Build federated remotes locally',\n run: (a) => require('../commands/publish').publishBuildCommand(a),\n },\n {\n name: 'publish:upload',\n summary: 'Upload built version dirs (nothing goes live)',\n run: (a) => require('../commands/publish').publishUploadCommand(a),\n },\n {\n name: 'publish:deploy',\n summary: 'Flip manifests to the uploaded versions',\n run: (a) => require('../commands/publish').publishDeployCommand(a),\n },\n {\n name: 'check:circular',\n summary: 'Scan workspace sources for circular imports (fails on cycles; --warn to only report)',\n run: (a) => require('../commands/checkCircular').checkCircularCommand(a),\n },\n {\n name: 'hooks',\n usageArgs: '<name>',\n summary: 'Run qpq:<name> (or plain <name>) in every app/package that defines it (dependency ordered, parallel; --jobs=N)',\n promptArgs: promptHookName,\n run: (a) => require('../commands/hooks').hooksCommand(a),\n },\n];\n/* eslint-enable @typescript-eslint/no-require-imports */\n\n// Width of the \"name [args]\" column in the usage text.\nconst usageColumnWidth = 17;\n\nexport const buildUsage = (): string => {\n const commandLines = cliCommands.flatMap((command) => {\n const nameAndArgs = command.usageArgs ? `${command.name} ${command.usageArgs}` : command.name;\n const extraLines = (command.usageExtra ?? []).map((line) => ` ${''.padEnd(usageColumnWidth)}${line}`);\n return [` ${nameAndArgs.padEnd(usageColumnWidth)}${command.summary}`, ...extraLines];\n });\n\n return [\n 'qpq — build, dev and deploy orchestration for QPQ apps',\n '',\n 'Usage: qpq <command> [--app <name>] [--env <name>] [--platform <name>]',\n ' qpq (no command opens an interactive menu)',\n '',\n 'Commands:',\n ...commandLines,\n '',\n ].join('\\n');\n};\n"]}
@@ -0,0 +1 @@
1
+ export declare const checkCircularCommand: (argv: string[]) => Promise<void>;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.checkCircularCommand = void 0;
13
+ // `qpq check:circular` — scan every workspace's src for circular imports (the
14
+ // class of bug that TDZ-crashes ESM bundles and silently drops exports under
15
+ // CJS). Cycles fail the command (exit 1) — they are never acceptable; --warn or
16
+ // QPQ_CIRCULAR_DEPS_WARN=1 is a temporary escape hatch that reports without
17
+ // failing while fixing.
18
+ const circularImports_1 = require("../lib/circularImports");
19
+ const discovery_1 = require("../lib/discovery");
20
+ // madge reports every distinct loop path; one tangled module can produce dozens
21
+ // of chains through the same files. Merge chains that share any file into one
22
+ // group so the report shows each tangle once, with its full blast radius.
23
+ const groupCycles = (cycles) => {
24
+ var _a;
25
+ const parent = new Map();
26
+ const find = (file) => {
27
+ var _a;
28
+ const found = (_a = parent.get(file)) !== null && _a !== void 0 ? _a : file;
29
+ if (found === file) {
30
+ return file;
31
+ }
32
+ const rootFile = find(found);
33
+ parent.set(file, rootFile);
34
+ return rootFile;
35
+ };
36
+ for (const cycle of cycles) {
37
+ for (const file of cycle) {
38
+ parent.set(find(file), find(cycle[0]));
39
+ }
40
+ }
41
+ const groups = new Map();
42
+ for (const cycle of cycles) {
43
+ const key = find(cycle[0]);
44
+ const group = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { files: [], chainCount: 0 };
45
+ group.chainCount += 1;
46
+ group.files = [...new Set([...group.files, ...cycle])];
47
+ groups.set(key, group);
48
+ }
49
+ return [...groups.values()];
50
+ };
51
+ const checkCircularCommand = (argv) => __awaiter(void 0, void 0, void 0, function* () {
52
+ const warnOnly = argv.includes('--warn') || !!process.env.QPQ_CIRCULAR_DEPS_WARN;
53
+ const cycles = yield (0, circularImports_1.findWorkspaceImportCycles)((0, discovery_1.getRoot)());
54
+ if (cycles.length === 0) {
55
+ console.log('check:circular: no circular imports found');
56
+ return;
57
+ }
58
+ const groups = groupCycles(cycles);
59
+ const colour = warnOnly ? '\x1b[33m' : '\x1b[31m';
60
+ for (const group of groups) {
61
+ const chainNote = group.chainCount > 1 ? `, ${group.chainCount} distinct loops` : '';
62
+ console.warn(`${colour}Circular imports (${group.files.length} files${chainNote}):\n ${group.files.join('\n ')}\x1b[0m`);
63
+ }
64
+ console.warn(`${colour}check:circular: ${groups.length} circular import group(s) found (${cycles.length} loops)\x1b[0m`);
65
+ if (!warnOnly) {
66
+ process.exitCode = 1;
67
+ }
68
+ });
69
+ exports.checkCircularCommand = checkCircularCommand;
70
+ //# sourceMappingURL=checkCircular.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkCircular.js","sourceRoot":"","sources":["../../../src/commands/checkCircular.ts"],"names":[],"mappings":"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","sourcesContent":["// `qpq check:circular` — scan every workspace's src for circular imports (the\n// class of bug that TDZ-crashes ESM bundles and silently drops exports under\n// CJS). Cycles fail the command (exit 1) — they are never acceptable; --warn or\n// QPQ_CIRCULAR_DEPS_WARN=1 is a temporary escape hatch that reports without\n// failing while fixing.\nimport { findWorkspaceImportCycles, ImportCycle } from '../lib/circularImports';\nimport { getRoot } from '../lib/discovery';\n\ntype CycleGroup = { files: string[]; chainCount: number };\n\n// madge reports every distinct loop path; one tangled module can produce dozens\n// of chains through the same files. Merge chains that share any file into one\n// group so the report shows each tangle once, with its full blast radius.\nconst groupCycles = (cycles: ImportCycle[]): CycleGroup[] => {\n const parent = new Map<string, string>();\n\n const find = (file: string): string => {\n const found = parent.get(file) ?? file;\n if (found === file) {\n return file;\n }\n const rootFile = find(found);\n parent.set(file, rootFile);\n return rootFile;\n };\n\n for (const cycle of cycles) {\n for (const file of cycle) {\n parent.set(find(file), find(cycle[0]));\n }\n }\n\n const groups = new Map<string, CycleGroup>();\n for (const cycle of cycles) {\n const key = find(cycle[0]);\n const group = groups.get(key) ?? { files: [], chainCount: 0 };\n group.chainCount += 1;\n group.files = [...new Set([...group.files, ...cycle])];\n groups.set(key, group);\n }\n\n return [...groups.values()];\n};\n\nexport const checkCircularCommand = async (argv: string[]): Promise<void> => {\n const warnOnly = argv.includes('--warn') || !!process.env.QPQ_CIRCULAR_DEPS_WARN;\n\n const cycles = await findWorkspaceImportCycles(getRoot());\n\n if (cycles.length === 0) {\n console.log('check:circular: no circular imports found');\n return;\n }\n\n const groups = groupCycles(cycles);\n\n const colour = warnOnly ? '\\x1b[33m' : '\\x1b[31m';\n for (const group of groups) {\n const chainNote = group.chainCount > 1 ? `, ${group.chainCount} distinct loops` : '';\n console.warn(`${colour}Circular imports (${group.files.length} files${chainNote}):\\n ${group.files.join('\\n ')}\\x1b[0m`);\n }\n console.warn(`${colour}check:circular: ${groups.length} circular import group(s) found (${cycles.length} loops)\\x1b[0m`);\n\n if (!warnOnly) {\n process.exitCode = 1;\n }\n};\n"]}
@@ -63,7 +63,18 @@ const synthCommand = (argv) => __awaiter(void 0, void 0, void 0, function* () {
63
63
  // invoked inside a deploy they're already primed; standalone synth primes
64
64
  // them from deploy.config.json (development default).
65
65
  (0, deployEnv_1.primeDeployEnvFromConfig)(appName);
66
- const serviceNames = specifiedServiceName ? [specifiedServiceName] : (0, discovery_1.getServiceNamesWithSubdir)(appName, 'service');
66
+ // Validate a specified service against the app's discovered services before it
67
+ // reaches loadServiceQpqConfig -> require. The bare-service-name branch above
68
+ // already checks this; the --app/env branch used to skip it, letting a typo hit
69
+ // a cryptic module-resolution error and a `../` value normalize into an arbitrary
70
+ // require target. Both paths now validate identically.
71
+ const appServices = (0, discovery_1.getServiceNamesWithSubdir)(appName, 'service');
72
+ if (specifiedServiceName && !appServices.includes(specifiedServiceName)) {
73
+ console.error(`Service '${specifiedServiceName}' not found in app '${appName}'`);
74
+ process.exitCode = 1;
75
+ continue;
76
+ }
77
+ const serviceNames = specifiedServiceName ? [specifiedServiceName] : appServices;
67
78
  for (const folderName of serviceNames) {
68
79
  try {
69
80
  processConfig(folderName, appName);
@@ -1 +1 @@
1
- {"version":3,"file":"synth.js","sourceRoot":"","sources":["../../../src/commands/synth.ts"],"names":[],"mappings":"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","sourcesContent":["// `qpq synth [service]` — synthesizes each backend service's QPQ config to\n// dist/apps/<app>/infrastructure/<module>-config.json.\n//\n// Runs with ts-node hooks registered (see runCli), which transpiles each\n// service's infrastructure.ts on require. Cross-package `@scope/*` imports\n// resolve through the workspace node_modules symlinks to already-built dist.\nimport { qpqCoreUtils } from 'quidproquo-core';\n\nimport fs from 'fs';\nimport path from 'path';\n\nimport { getPositionalArgs } from '../lib/args';\nimport { primeDeployEnvFromConfig } from '../lib/deployEnv';\nimport { getAvailableApps, getRoot, getServiceNamesWithSubdir } from '../lib/discovery';\nimport { loadServiceQpqConfig } from '../lib/qpqConfigs';\nimport { resolveAppSelection } from '../lib/resolveAppSelection';\n\nconst processConfig = (folderName: string, appName: string): void => {\n console.log(`Processing infrastructure: [${folderName}] for app [${appName}]`);\n const qpqConfig = loadServiceQpqConfig(appName, folderName);\n const moduleName = qpqCoreUtils.getApplicationModuleName(qpqConfig);\n\n const directoryPath = path.join(getRoot(), 'dist', 'apps', appName, 'infrastructure');\n fs.mkdirSync(directoryPath, { recursive: true });\n fs.writeFileSync(path.join(directoryPath, `${moduleName}-config.json`), JSON.stringify(qpqConfig, null, 2));\n console.log(` -> ${appName}/infrastructure/${moduleName}-config.json`);\n};\n\nexport const synthCommand = async (argv: string[]): Promise<void> => {\n const [specifiedServiceName] = getPositionalArgs(argv, ['--app', '--env']);\n\n const hasExplicitApp =\n argv.includes('--app') || argv.some((a) => a.startsWith('--app=')) || !!process.env.npm_config_app || !!process.env.DEPLOY_APP_NAME;\n\n let appNames: string[];\n if (specifiedServiceName && !hasExplicitApp) {\n // A bare service name is unambiguous — synth it in whichever app owns it.\n const appWithService = getAvailableApps().find((appName) => getServiceNamesWithSubdir(appName, 'service').includes(specifiedServiceName));\n if (!appWithService) {\n console.error(`Service '${specifiedServiceName}' not found in any app`);\n process.exit(1);\n }\n appNames = [appWithService];\n } else {\n const selection = await resolveAppSelection({\n argv,\n envVar: 'DEPLOY_APP_NAME',\n allowAll: true,\n });\n appNames = selection === 'all' ? getAvailableApps() : [selection];\n }\n\n console.log(`QPQ synthesizing apps: ${appNames.join(', ')}`);\n\n for (const appName of appNames) {\n // Service configs read ENVIRONMENT / AWS_DEFAULT_* at require time. When\n // invoked inside a deploy they're already primed; standalone synth primes\n // them from deploy.config.json (development default).\n primeDeployEnvFromConfig(appName);\n\n const serviceNames = specifiedServiceName ? [specifiedServiceName] : getServiceNamesWithSubdir(appName, 'service');\n\n for (const folderName of serviceNames) {\n try {\n processConfig(folderName, appName);\n } catch (error) {\n console.error(`Failed to process ${folderName} in ${appName}:`, error);\n process.exitCode = 1;\n }\n }\n }\n};\n"]}
1
+ {"version":3,"file":"synth.js","sourceRoot":"","sources":["../../../src/commands/synth.ts"],"names":[],"mappings":"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","sourcesContent":["// `qpq synth [service]` — synthesizes each backend service's QPQ config to\n// dist/apps/<app>/infrastructure/<module>-config.json.\n//\n// Runs with ts-node hooks registered (see runCli), which transpiles each\n// service's infrastructure.ts on require. Cross-package `@scope/*` imports\n// resolve through the workspace node_modules symlinks to already-built dist.\nimport { qpqCoreUtils } from 'quidproquo-core';\n\nimport fs from 'fs';\nimport path from 'path';\n\nimport { getPositionalArgs } from '../lib/args';\nimport { primeDeployEnvFromConfig } from '../lib/deployEnv';\nimport { getAvailableApps, getRoot, getServiceNamesWithSubdir } from '../lib/discovery';\nimport { loadServiceQpqConfig } from '../lib/qpqConfigs';\nimport { resolveAppSelection } from '../lib/resolveAppSelection';\n\nconst processConfig = (folderName: string, appName: string): void => {\n console.log(`Processing infrastructure: [${folderName}] for app [${appName}]`);\n const qpqConfig = loadServiceQpqConfig(appName, folderName);\n const moduleName = qpqCoreUtils.getApplicationModuleName(qpqConfig);\n\n const directoryPath = path.join(getRoot(), 'dist', 'apps', appName, 'infrastructure');\n fs.mkdirSync(directoryPath, { recursive: true });\n fs.writeFileSync(path.join(directoryPath, `${moduleName}-config.json`), JSON.stringify(qpqConfig, null, 2));\n console.log(` -> ${appName}/infrastructure/${moduleName}-config.json`);\n};\n\nexport const synthCommand = async (argv: string[]): Promise<void> => {\n const [specifiedServiceName] = getPositionalArgs(argv, ['--app', '--env']);\n\n const hasExplicitApp =\n argv.includes('--app') || argv.some((a) => a.startsWith('--app=')) || !!process.env.npm_config_app || !!process.env.DEPLOY_APP_NAME;\n\n let appNames: string[];\n if (specifiedServiceName && !hasExplicitApp) {\n // A bare service name is unambiguous — synth it in whichever app owns it.\n const appWithService = getAvailableApps().find((appName) => getServiceNamesWithSubdir(appName, 'service').includes(specifiedServiceName));\n if (!appWithService) {\n console.error(`Service '${specifiedServiceName}' not found in any app`);\n process.exit(1);\n }\n appNames = [appWithService];\n } else {\n const selection = await resolveAppSelection({\n argv,\n envVar: 'DEPLOY_APP_NAME',\n allowAll: true,\n });\n appNames = selection === 'all' ? getAvailableApps() : [selection];\n }\n\n console.log(`QPQ synthesizing apps: ${appNames.join(', ')}`);\n\n for (const appName of appNames) {\n // Service configs read ENVIRONMENT / AWS_DEFAULT_* at require time. When\n // invoked inside a deploy they're already primed; standalone synth primes\n // them from deploy.config.json (development default).\n primeDeployEnvFromConfig(appName);\n\n // Validate a specified service against the app's discovered services before it\n // reaches loadServiceQpqConfig -> require. The bare-service-name branch above\n // already checks this; the --app/env branch used to skip it, letting a typo hit\n // a cryptic module-resolution error and a `../` value normalize into an arbitrary\n // require target. Both paths now validate identically.\n const appServices = getServiceNamesWithSubdir(appName, 'service');\n if (specifiedServiceName && !appServices.includes(specifiedServiceName)) {\n console.error(`Service '${specifiedServiceName}' not found in app '${appName}'`);\n process.exitCode = 1;\n continue;\n }\n\n const serviceNames = specifiedServiceName ? [specifiedServiceName] : appServices;\n\n for (const folderName of serviceNames) {\n try {\n processConfig(folderName, appName);\n } catch (error) {\n console.error(`Failed to process ${folderName} in ${appName}:`, error);\n process.exitCode = 1;\n }\n }\n }\n};\n"]}
@@ -0,0 +1,3 @@
1
+ export type ImportCycle = string[];
2
+ export declare const getWorkspaceDirectories: (root: string) => string[];
3
+ export declare const findWorkspaceImportCycles: (root: string) => Promise<ImportCycle[]>;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.findWorkspaceImportCycles = exports.getWorkspaceDirectories = void 0;
16
+ // Source-level circular import detection over the consumer repo's npm
17
+ // workspaces, powered by madge (AST-based dependency graph + cycle detection).
18
+ // Complements the rspack CircularCheckRspackPlugin (which needs a bundle to
19
+ // run): this scans TypeScript sources directly, so it can run inside tsc-only
20
+ // flows like `npm run validate-ts` with no bundling.
21
+ //
22
+ // Scope: each workspace's src, relative imports. Cross-package cycles are
23
+ // already impossible — TypeScript project references refuse to build them — so
24
+ // every runtime-relevant cycle lives inside one package's src. Type-only
25
+ // imports are skipped (skipTypeImports): tsc erases them, so they never create
26
+ // a runtime cycle.
27
+ const fs_1 = __importDefault(require("fs"));
28
+ const path_1 = __importDefault(require("path"));
29
+ // madge ships no type definitions; declare the narrow slice we use.
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
31
+ const madge = require('madge');
32
+ // Expand npm workspace globs (plain path segments and `*` segments only — the
33
+ // only forms npm workspaces use in practice) into existing directories.
34
+ const expandWorkspaceGlob = (root, pattern) => {
35
+ let candidates = [root];
36
+ for (const segment of pattern.split('/')) {
37
+ if (segment === '*') {
38
+ candidates = candidates.flatMap((dir) => fs_1.default.existsSync(dir)
39
+ ? fs_1.default
40
+ .readdirSync(dir, { withFileTypes: true })
41
+ .filter((entry) => entry.isDirectory())
42
+ .map((entry) => path_1.default.join(dir, entry.name))
43
+ : []);
44
+ }
45
+ else {
46
+ candidates = candidates.map((dir) => path_1.default.join(dir, segment));
47
+ }
48
+ }
49
+ return candidates.filter((dir) => fs_1.default.existsSync(dir) && fs_1.default.statSync(dir).isDirectory());
50
+ };
51
+ const getWorkspaceDirectories = (root) => {
52
+ var _a;
53
+ const packageJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'package.json'), 'utf8'));
54
+ const patterns = (_a = packageJson.workspaces) !== null && _a !== void 0 ? _a : [];
55
+ return patterns.flatMap((pattern) => expandWorkspaceGlob(root, pattern));
56
+ };
57
+ exports.getWorkspaceDirectories = getWorkspaceDirectories;
58
+ const findWorkspaceImportCycles = (root) => __awaiter(void 0, void 0, void 0, function* () {
59
+ const sourceDirs = (0, exports.getWorkspaceDirectories)(root)
60
+ .map((dir) => path_1.default.join(dir, 'src'))
61
+ .filter((dir) => fs_1.default.existsSync(dir));
62
+ // Excludes: node_modules and built output (dist/lib), declaration files
63
+ // (type-only, can't cycle at runtime), and anything madge resolves to outside
64
+ // the repo root (linked framework packages — their cycles surface through the
65
+ // rspack CircularCheckRspackPlugin instead, this command owns the consumer
66
+ // repo). Out-of-root paths start with '..' because baseDir is the repo root.
67
+ const result = yield madge(sourceDirs, {
68
+ baseDir: root,
69
+ fileExtensions: ['ts', 'tsx'],
70
+ excludeRegExp: [/node_modules/, /(^|\/)(dist|lib)\//, /\.d\.ts$/, /^\.\./],
71
+ detectiveOptions: {
72
+ ts: { skipTypeImports: true },
73
+ tsx: { skipTypeImports: true },
74
+ },
75
+ });
76
+ return result.circular();
77
+ });
78
+ exports.findWorkspaceImportCycles = findWorkspaceImportCycles;
79
+ //# sourceMappingURL=circularImports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circularImports.js","sourceRoot":"","sources":["../../../src/lib/circularImports.ts"],"names":[],"mappings":"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","sourcesContent":["// Source-level circular import detection over the consumer repo's npm\n// workspaces, powered by madge (AST-based dependency graph + cycle detection).\n// Complements the rspack CircularCheckRspackPlugin (which needs a bundle to\n// run): this scans TypeScript sources directly, so it can run inside tsc-only\n// flows like `npm run validate-ts` with no bundling.\n//\n// Scope: each workspace's src, relative imports. Cross-package cycles are\n// already impossible — TypeScript project references refuse to build them — so\n// every runtime-relevant cycle lives inside one package's src. Type-only\n// imports are skipped (skipTypeImports): tsc erases them, so they never create\n// a runtime cycle.\nimport fs from 'fs';\nimport path from 'path';\n\n// madge ships no type definitions; declare the narrow slice we use.\n// eslint-disable-next-line @typescript-eslint/no-require-imports\nconst madge = require('madge') as (paths: string[], config: object) => Promise<{ circular(): string[][] }>;\n\nexport type ImportCycle = string[];\n\n// Expand npm workspace globs (plain path segments and `*` segments only — the\n// only forms npm workspaces use in practice) into existing directories.\nconst expandWorkspaceGlob = (root: string, pattern: string): string[] => {\n let candidates = [root];\n\n for (const segment of pattern.split('/')) {\n if (segment === '*') {\n candidates = candidates.flatMap((dir) =>\n fs.existsSync(dir)\n ? fs\n .readdirSync(dir, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => path.join(dir, entry.name))\n : [],\n );\n } else {\n candidates = candidates.map((dir) => path.join(dir, segment));\n }\n }\n\n return candidates.filter((dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory());\n};\n\nexport const getWorkspaceDirectories = (root: string): string[] => {\n const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));\n const patterns: string[] = packageJson.workspaces ?? [];\n return patterns.flatMap((pattern) => expandWorkspaceGlob(root, pattern));\n};\n\nexport const findWorkspaceImportCycles = async (root: string): Promise<ImportCycle[]> => {\n const sourceDirs = getWorkspaceDirectories(root)\n .map((dir) => path.join(dir, 'src'))\n .filter((dir) => fs.existsSync(dir));\n\n // Excludes: node_modules and built output (dist/lib), declaration files\n // (type-only, can't cycle at runtime), and anything madge resolves to outside\n // the repo root (linked framework packages — their cycles surface through the\n // rspack CircularCheckRspackPlugin instead, this command owns the consumer\n // repo). Out-of-root paths start with '..' because baseDir is the repo root.\n const result = await madge(sourceDirs, {\n baseDir: root,\n fileExtensions: ['ts', 'tsx'],\n excludeRegExp: [/node_modules/, /(^|\\/)(dist|lib)\\//, /\\.d\\.ts$/, /^\\.\\./],\n detectiveOptions: {\n ts: { skipTypeImports: true },\n tsx: { skipTypeImports: true },\n },\n });\n\n return result.circular();\n};\n"]}
@@ -4,6 +4,7 @@ export type RunCommandOptions = {
4
4
  logCommand?: boolean;
5
5
  };
6
6
  export declare const runCommand: (command: string, args: string[], options?: RunCommandOptions) => Promise<void>;
7
+ export declare const runCommandBestEffort: (command: string, args: string[], options?: RunCommandOptions) => Promise<void>;
7
8
  export type RunCommandPrefixedOptions = {
8
9
  env?: Record<string, string>;
9
10
  cwd?: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runCommandPrefixed = exports.nextPrefixColor = exports.runCommand = void 0;
3
+ exports.runCommandPrefixed = exports.nextPrefixColor = exports.runCommandBestEffort = exports.runCommand = void 0;
4
4
  const child_process_1 = require("child_process");
5
5
  // Spawn a command, inheriting stdio, resolving on success. A non-zero exit
6
6
  // REJECTS so a failed build/deploy stops the run.
@@ -12,7 +12,11 @@ const runCommand = (command, args, options = {}) => new Promise((resolve, reject
12
12
  }
13
13
  const child = (0, child_process_1.spawn)(command, validArgs, {
14
14
  stdio: 'inherit',
15
- shell: true,
15
+ // Shell only on Windows (to resolve npm/aws/cdk .cmd shims). On posix, run
16
+ // without a shell so the already-array args are passed straight to the
17
+ // program and can't be re-interpreted by `sh -c` — no command injection via
18
+ // interpolated values (stack names, CI-supplied env, etc.).
19
+ shell: process.platform === 'win32',
16
20
  cwd: (_a = options.cwd) !== null && _a !== void 0 ? _a : process.cwd(),
17
21
  env: Object.assign(Object.assign({}, process.env), ((_b = options.env) !== null && _b !== void 0 ? _b : {})),
18
22
  });
@@ -26,6 +30,23 @@ const runCommand = (command, args, options = {}) => new Promise((resolve, reject
26
30
  });
27
31
  });
28
32
  exports.runCommand = runCommand;
33
+ // Run a command for its side effect only, swallowing both its output and its
34
+ // exit code — for best-effort cleanup steps (e.g. `docker rm -f` of a container
35
+ // that may not exist) that must never fail the run. Without a shell there's no
36
+ // `>/dev/null 2>&1 || true`, so quieting and non-failing happen here instead.
37
+ const runCommandBestEffort = (command, args, options = {}) => new Promise((resolve) => {
38
+ var _a, _b;
39
+ const validArgs = args.filter((a) => !!a);
40
+ const child = (0, child_process_1.spawn)(command, validArgs, {
41
+ stdio: 'ignore',
42
+ shell: process.platform === 'win32',
43
+ cwd: (_a = options.cwd) !== null && _a !== void 0 ? _a : process.cwd(),
44
+ env: Object.assign(Object.assign({}, process.env), ((_b = options.env) !== null && _b !== void 0 ? _b : {})),
45
+ });
46
+ child.on('error', () => resolve());
47
+ child.on('exit', () => resolve());
48
+ });
49
+ exports.runCommandBestEffort = runCommandBestEffort;
29
50
  const PREFIX_COLORS = [36, 32, 33, 35, 34, 96, 92, 93, 95, 94];
30
51
  let colorIndex = 0;
31
52
  // Hand each concurrent task a distinct prefix color so interleaved output is
@@ -45,7 +66,11 @@ const runCommandPrefixed = (label, command, args, options = {}) => new Promise((
45
66
  const prefix = `\x1b[${(_a = options.color) !== null && _a !== void 0 ? _a : 36}m[${label}]\x1b[0m `;
46
67
  const child = (0, child_process_1.spawn)(command, validArgs, {
47
68
  stdio: ['ignore', 'pipe', 'pipe'],
48
- shell: true,
69
+ // Shell only on Windows (to resolve npm/aws/cdk .cmd shims). On posix, run
70
+ // without a shell so the already-array args are passed straight to the
71
+ // program and can't be re-interpreted by `sh -c` — no command injection via
72
+ // interpolated values (stack names, CI-supplied env, etc.).
73
+ shell: process.platform === 'win32',
49
74
  cwd: (_b = options.cwd) !== null && _b !== void 0 ? _b : process.cwd(),
50
75
  env: Object.assign(Object.assign({}, process.env), ((_c = options.env) !== null && _c !== void 0 ? _c : {})),
51
76
  });
@@ -1 +1 @@
1
- {"version":3,"file":"runCommand.js","sourceRoot":"","sources":["../../../src/lib/runCommand.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'child_process';\nimport type { Readable } from 'stream';\n\nexport type RunCommandOptions = {\n // Extra environment variables merged over the parent process env.\n env?: Record<string, string>;\n // Working directory for the child (defaults to cwd).\n cwd?: string;\n // Echo the command before running it.\n logCommand?: boolean;\n};\n\n// Spawn a command, inheriting stdio, resolving on success. A non-zero exit\n// REJECTS so a failed build/deploy stops the run.\nexport const runCommand = (command: string, args: string[], options: RunCommandOptions = {}): Promise<void> =>\n new Promise((resolve, reject) => {\n const validArgs = args.filter((a) => !!a);\n\n if (options.logCommand) {\n console.log(`Running command: ${command} ${validArgs.join(' ')}`);\n }\n\n const child = spawn(command, validArgs, {\n stdio: 'inherit',\n shell: true,\n cwd: options.cwd ?? process.cwd(),\n env: { ...process.env, ...(options.env ?? {}) },\n });\n\n child.on('error', (error) => reject(error));\n\n child.on('exit', (code, signal) => {\n if (signal) return reject(new Error(`${command} was killed with signal ${signal}`));\n if (code) return reject(new Error(`${command} ${validArgs.join(' ')} exited with code ${code}`));\n return resolve();\n });\n });\n\nexport type RunCommandPrefixedOptions = {\n env?: Record<string, string>;\n cwd?: string;\n // ANSI color code for the prefix (see nextPrefixColor).\n color?: number;\n};\n\nconst PREFIX_COLORS = [36, 32, 33, 35, 34, 96, 92, 93, 95, 94];\nlet colorIndex = 0;\n\n// Hand each concurrent task a distinct prefix color so interleaved output is\n// scannable.\nexport const nextPrefixColor = (): number => {\n const color = PREFIX_COLORS[colorIndex % PREFIX_COLORS.length];\n colorIndex += 1;\n return color;\n};\n\n// Like runCommand, but pipes output and prefixes every line with a colored\n// [label] — for running many commands concurrently without unreadable\n// interleaving. Rejects on non-zero exit.\nexport const runCommandPrefixed = (label: string, command: string, args: string[], options: RunCommandPrefixedOptions = {}): Promise<void> =>\n new Promise((resolve, reject) => {\n const validArgs = args.filter((a) => !!a);\n const prefix = `\\x1b[${options.color ?? 36}m[${label}]\\x1b[0m `;\n\n const child = spawn(command, validArgs, {\n stdio: ['ignore', 'pipe', 'pipe'],\n shell: true,\n cwd: options.cwd ?? process.cwd(),\n env: { ...process.env, ...(options.env ?? {}) },\n });\n\n const attach = (stream: Readable, out: NodeJS.WriteStream): void => {\n let buffered = '';\n stream.on('data', (chunk: Buffer) => {\n buffered += chunk.toString();\n const lines = buffered.split('\\n');\n buffered = lines.pop() ?? '';\n lines.forEach((line) => out.write(`${prefix}${line}\\n`));\n });\n stream.on('end', () => {\n if (buffered) out.write(`${prefix}${buffered}\\n`);\n });\n };\n\n attach(child.stdout as Readable, process.stdout);\n attach(child.stderr as Readable, process.stderr);\n\n child.on('error', (error) => reject(error));\n\n child.on('exit', (code, signal) => {\n if (signal) return reject(new Error(`[${label}] ${command} was killed with signal ${signal}`));\n if (code) return reject(new Error(`[${label}] ${command} exited with code ${code}`));\n return resolve();\n });\n });\n"]}
1
+ {"version":3,"file":"runCommand.js","sourceRoot":"","sources":["../../../src/lib/runCommand.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'child_process';\nimport type { Readable } from 'stream';\n\nexport type RunCommandOptions = {\n // Extra environment variables merged over the parent process env.\n env?: Record<string, string>;\n // Working directory for the child (defaults to cwd).\n cwd?: string;\n // Echo the command before running it.\n logCommand?: boolean;\n};\n\n// Spawn a command, inheriting stdio, resolving on success. A non-zero exit\n// REJECTS so a failed build/deploy stops the run.\nexport const runCommand = (command: string, args: string[], options: RunCommandOptions = {}): Promise<void> =>\n new Promise((resolve, reject) => {\n const validArgs = args.filter((a) => !!a);\n\n if (options.logCommand) {\n console.log(`Running command: ${command} ${validArgs.join(' ')}`);\n }\n\n const child = spawn(command, validArgs, {\n stdio: 'inherit',\n // Shell only on Windows (to resolve npm/aws/cdk .cmd shims). On posix, run\n // without a shell so the already-array args are passed straight to the\n // program and can't be re-interpreted by `sh -c` — no command injection via\n // interpolated values (stack names, CI-supplied env, etc.).\n shell: process.platform === 'win32',\n cwd: options.cwd ?? process.cwd(),\n env: { ...process.env, ...(options.env ?? {}) },\n });\n\n child.on('error', (error) => reject(error));\n\n child.on('exit', (code, signal) => {\n if (signal) return reject(new Error(`${command} was killed with signal ${signal}`));\n if (code) return reject(new Error(`${command} ${validArgs.join(' ')} exited with code ${code}`));\n return resolve();\n });\n });\n\n// Run a command for its side effect only, swallowing both its output and its\n// exit code — for best-effort cleanup steps (e.g. `docker rm -f` of a container\n// that may not exist) that must never fail the run. Without a shell there's no\n// `>/dev/null 2>&1 || true`, so quieting and non-failing happen here instead.\nexport const runCommandBestEffort = (command: string, args: string[], options: RunCommandOptions = {}): Promise<void> =>\n new Promise((resolve) => {\n const validArgs = args.filter((a) => !!a);\n\n const child = spawn(command, validArgs, {\n stdio: 'ignore',\n shell: process.platform === 'win32',\n cwd: options.cwd ?? process.cwd(),\n env: { ...process.env, ...(options.env ?? {}) },\n });\n\n child.on('error', () => resolve());\n child.on('exit', () => resolve());\n });\n\nexport type RunCommandPrefixedOptions = {\n env?: Record<string, string>;\n cwd?: string;\n // ANSI color code for the prefix (see nextPrefixColor).\n color?: number;\n};\n\nconst PREFIX_COLORS = [36, 32, 33, 35, 34, 96, 92, 93, 95, 94];\nlet colorIndex = 0;\n\n// Hand each concurrent task a distinct prefix color so interleaved output is\n// scannable.\nexport const nextPrefixColor = (): number => {\n const color = PREFIX_COLORS[colorIndex % PREFIX_COLORS.length];\n colorIndex += 1;\n return color;\n};\n\n// Like runCommand, but pipes output and prefixes every line with a colored\n// [label] — for running many commands concurrently without unreadable\n// interleaving. Rejects on non-zero exit.\nexport const runCommandPrefixed = (label: string, command: string, args: string[], options: RunCommandPrefixedOptions = {}): Promise<void> =>\n new Promise((resolve, reject) => {\n const validArgs = args.filter((a) => !!a);\n const prefix = `\\x1b[${options.color ?? 36}m[${label}]\\x1b[0m `;\n\n const child = spawn(command, validArgs, {\n stdio: ['ignore', 'pipe', 'pipe'],\n // Shell only on Windows (to resolve npm/aws/cdk .cmd shims). On posix, run\n // without a shell so the already-array args are passed straight to the\n // program and can't be re-interpreted by `sh -c` — no command injection via\n // interpolated values (stack names, CI-supplied env, etc.).\n shell: process.platform === 'win32',\n cwd: options.cwd ?? process.cwd(),\n env: { ...process.env, ...(options.env ?? {}) },\n });\n\n const attach = (stream: Readable, out: NodeJS.WriteStream): void => {\n let buffered = '';\n stream.on('data', (chunk: Buffer) => {\n buffered += chunk.toString();\n const lines = buffered.split('\\n');\n buffered = lines.pop() ?? '';\n lines.forEach((line) => out.write(`${prefix}${line}\\n`));\n });\n stream.on('end', () => {\n if (buffered) out.write(`${prefix}${buffered}\\n`);\n });\n };\n\n attach(child.stdout as Readable, process.stdout);\n attach(child.stderr as Readable, process.stderr);\n\n child.on('error', (error) => reject(error));\n\n child.on('exit', (code, signal) => {\n if (signal) return reject(new Error(`[${label}] ${command} was killed with signal ${signal}`));\n if (code) return reject(new Error(`[${label}] ${command} exited with code ${code}`));\n return resolve();\n });\n });\n"]}
@@ -73,9 +73,14 @@ const AWS_ENV_PASSTHROUGH = [
73
73
  'AWS_DEFAULT_ACCOUNT',
74
74
  ];
75
75
  const assemblyDir = (appName, serviceName) => path_1.default.join((0, discovery_1.getRoot)(), 'dist', 'qpq', 'cdk-docker', appName, serviceName);
76
+ // A container left behind by an interrupted run would block reusing its name.
77
+ // Clear it first, best-effort: a missing container (exit 1) must not fail the
78
+ // deploy. Was a `docker rm -f … >/dev/null 2>&1 ;` prefix on the run command,
79
+ // but that only worked under `shell: true`; as a separate call it's shell-free.
80
+ const removeContainer = (containerName) => (0, runCommand_1.runCommandBestEffort)('docker', ['rm', '-f', containerName]);
76
81
  const assertDockerRunning = () => __awaiter(void 0, void 0, void 0, function* () {
77
82
  try {
78
- yield (0, runCommand_1.runCommand)('docker', ['info', '--format', '"docker daemon ok — server {{.ServerVersion}}"']);
83
+ yield (0, runCommand_1.runCommand)('docker', ['info', '--format', 'docker daemon ok — server {{.ServerVersion}}']);
79
84
  }
80
85
  catch (_a) {
81
86
  throw new Error('Docker daemon is not running (start Docker Desktop and retry).');
@@ -94,59 +99,62 @@ const buildDeployerImage = () => __awaiter(void 0, void 0, void 0, function* ()
94
99
  // The assembly mounts read-only and is copied container-local first — the CDK
95
100
  // CLI writes lock files next to the assembly it reads, and the copy keeps
96
101
  // parallel api/web deploys of the same service fully isolated.
97
- const dockerDeployStack = (color, appName, serviceName, stackType, stackName) => {
102
+ const dockerDeployStack = (color, appName, serviceName, stackType, stackName) => __awaiter(void 0, void 0, void 0, function* () {
98
103
  const containerName = `${serviceName}-${stackType}`;
99
- return (0, runCommand_1.runCommandPrefixed)(`${stackType}:${serviceName}`, 'docker', [
100
- // A container left behind by an interrupted run would block reusing the
101
- // name — clear it quietly first.
102
- 'rm',
103
- '-f',
104
- containerName,
105
- '>/dev/null',
106
- '2>&1',
107
- ';',
108
- 'docker',
104
+ yield removeContainer(containerName);
105
+ yield (0, runCommand_1.runCommandPrefixed)(`${stackType}:${serviceName}`, 'docker', [
109
106
  'run',
110
107
  '--rm',
111
108
  '--name',
112
109
  containerName,
113
110
  '-v',
114
- `"${assemblyDir(appName, serviceName)}:/assembly:ro"`,
111
+ `${assemblyDir(appName, serviceName)}:/assembly:ro`,
115
112
  ...AWS_ENV_PASSTHROUGH.flatMap((name) => ['-e', name]),
116
113
  IMAGE_TAG,
117
114
  'bash',
118
115
  '-c',
119
- `"cp -r /assembly /work/assembly && cdk deploy ${stackName} --app /work/assembly --require-approval never --progress events"`,
116
+ `cp -r /assembly /work/assembly && cdk deploy ${stackName} --app /work/assembly --require-approval never --progress events`,
120
117
  ], { color });
121
- };
118
+ });
122
119
  // Sync one service's pre-built views to S3 in a throwaway aws-cli container
123
120
  // named <service>-views. The official image's entrypoint is `aws`. Multiple
124
121
  // destinations (shell → website root + views prefix) run sequentially within
125
- // the service, reusing the container name.
122
+ // the service, reusing the container name. Mirrors the host-side syncViewsToS3
123
+ // cache policy: hashed assets immutable, the fixed-name entry points re-uploaded
124
+ // with no-cache every deploy (see NO_CACHE_FILES).
126
125
  const dockerSyncViews = (color, appName, serviceName) => __awaiter(void 0, void 0, void 0, function* () {
127
126
  const containerName = `${serviceName}-views`;
128
- for (const destination of (0, viewsSync_1.getViewsS3Destinations)(appName, serviceName)) {
127
+ const distDir = (0, views_1.getViewsDistDir)(appName, serviceName);
128
+ const runAwsCli = (awsArgs) => __awaiter(void 0, void 0, void 0, function* () {
129
+ yield removeContainer(containerName);
129
130
  yield (0, runCommand_1.runCommandPrefixed)(`views:${serviceName}`, 'docker', [
130
- 'rm',
131
- '-f',
132
- containerName,
133
- '>/dev/null',
134
- '2>&1',
135
- ';',
136
- 'docker',
137
131
  'run',
138
132
  '--rm',
139
133
  '--name',
140
134
  containerName,
141
135
  '-v',
142
- `"${(0, views_1.getViewsDistDir)(appName, serviceName)}:/views:ro"`,
136
+ `${distDir}:/views:ro`,
143
137
  ...AWS_ENV_PASSTHROUGH.flatMap((name) => ['-e', name]),
144
138
  AWS_CLI_IMAGE,
139
+ ...awsArgs,
140
+ ], { color });
141
+ });
142
+ for (const destination of (0, viewsSync_1.getViewsS3Destinations)(appName, serviceName)) {
143
+ yield runAwsCli([
145
144
  's3',
146
145
  'sync',
147
146
  '/views',
148
147
  destination,
149
- ], { color });
148
+ '--cache-control',
149
+ 'public,max-age=31536000,immutable',
150
+ ...viewsSync_1.NO_CACHE_FILES.flatMap((file) => ['--exclude', file]),
151
+ ]);
152
+ for (const file of viewsSync_1.NO_CACHE_FILES) {
153
+ if (!fs_1.default.existsSync(path_1.default.join(distDir, file))) {
154
+ continue;
155
+ }
156
+ yield runAwsCli(['s3', 'cp', `/views/${file}`, `${destination}/${file}`, '--cache-control', 'no-cache']);
157
+ }
150
158
  }
151
159
  });
152
160
  // Publish one service's already-built federated remote to S3 in a throwaway
@@ -165,20 +173,14 @@ const dockerPublishRemote = (color, appName, serviceName) => __awaiter(void 0, v
165
173
  ['s3', 'cp', '/publish/manifest.json', `s3://${bucketName}/${servicePrefix}/manifest.json`],
166
174
  ];
167
175
  for (const s3Operation of s3Operations) {
176
+ yield removeContainer(containerName);
168
177
  yield (0, runCommand_1.runCommandPrefixed)(`publish:${serviceName}`, 'docker', [
169
- 'rm',
170
- '-f',
171
- containerName,
172
- '>/dev/null',
173
- '2>&1',
174
- ';',
175
- 'docker',
176
178
  'run',
177
179
  '--rm',
178
180
  '--name',
179
181
  containerName,
180
182
  '-v',
181
- `"${publishPath}:/publish:ro"`,
183
+ `${publishPath}:/publish:ro`,
182
184
  ...AWS_ENV_PASSTHROUGH.flatMap((name) => ['-e', name]),
183
185
  AWS_CLI_IMAGE,
184
186
  ...s3Operation,
@@ -287,7 +289,7 @@ const awsGoDocker = (appName, plan) => __awaiter(void 0, void 0, void 0, functio
287
289
  run: () => __awaiter(void 0, void 0, void 0, function* () {
288
290
  const outputDir = assemblyDir(appName, service);
289
291
  fs_1.default.rmSync(outputDir, { recursive: true, force: true });
290
- yield (0, runCommand_1.runCommandPrefixed)(`cdk-synth:${service}`, 'npx', ['cdk', 'synth', '--quiet', '--app', `'${(0, cdkApp_1.getCdkAppCommand)()}'`, '--output', `"${outputDir}"`], {
292
+ yield (0, runCommand_1.runCommandPrefixed)(`cdk-synth:${service}`, 'npx', ['cdk', 'synth', '--quiet', '--app', (0, cdkApp_1.getCdkAppCommand)(), '--output', outputDir], {
291
293
  cwd: (0, discovery_1.getRoot)(),
292
294
  env: { DEPLOY_SERVICE_NAME: service, DEPLOY_APP_NAME: appName },
293
295
  color: (0, runCommand_1.nextPrefixColor)(),