quidproquo-cli 0.1.7 → 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.
- package/docker/dev-server/Dockerfile +7 -9
- package/lib/commonjs/cli/commandMenu.d.ts +2 -0
- package/lib/commonjs/cli/commandMenu.js +81 -0
- package/lib/commonjs/cli/commandMenu.js.map +1 -0
- package/lib/commonjs/cli/commandRegistry.d.ts +10 -0
- package/lib/commonjs/cli/commandRegistry.js +131 -0
- package/lib/commonjs/cli/commandRegistry.js.map +1 -0
- package/lib/commonjs/cli/runCli.js +49 -51
- package/lib/commonjs/cli/runCli.js.map +1 -1
- package/lib/commonjs/commands/clearResources.d.ts +1 -0
- package/lib/commonjs/commands/clearResources.js +36 -0
- package/lib/commonjs/commands/clearResources.js.map +1 -0
- package/lib/commonjs/commands/go.js +3 -1
- package/lib/commonjs/commands/go.js.map +1 -1
- package/lib/commonjs/commands/goDev.js +11 -2
- package/lib/commonjs/commands/goDev.js.map +1 -1
- package/lib/commonjs/commands/goDevApi.js +6 -39
- package/lib/commonjs/commands/goDevApi.js.map +1 -1
- package/lib/commonjs/commands/goDevWeb.js +2 -0
- package/lib/commonjs/commands/goDevWeb.js.map +1 -1
- package/lib/commonjs/commands/goDocker.js +3 -1
- package/lib/commonjs/commands/goDocker.js.map +1 -1
- package/lib/commonjs/commands/hooks.js +23 -5
- package/lib/commonjs/commands/hooks.js.map +1 -1
- package/lib/commonjs/lib/clearResourcesPlan.d.ts +15 -0
- package/lib/commonjs/lib/clearResourcesPlan.js +80 -0
- package/lib/commonjs/lib/clearResourcesPlan.js.map +1 -0
- package/lib/commonjs/lib/deployPrompts.d.ts +1 -0
- package/lib/commonjs/lib/deployPrompts.js +40 -2
- package/lib/commonjs/lib/deployPrompts.js.map +1 -1
- package/lib/commonjs/lib/devServerEntry.js +3 -0
- package/lib/commonjs/lib/devServerEntry.js.map +1 -1
- package/lib/commonjs/lib/hooks.d.ts +2 -1
- package/lib/commonjs/lib/hooks.js +100 -5
- package/lib/commonjs/lib/hooks.js.map +1 -1
- package/lib/commonjs/lib/killStaleListeners.d.ts +3 -0
- package/lib/commonjs/lib/killStaleListeners.js +170 -0
- package/lib/commonjs/lib/killStaleListeners.js.map +1 -0
- package/lib/commonjs/lib/prompts.d.ts +10 -0
- package/lib/commonjs/lib/prompts.js +15 -1
- package/lib/commonjs/lib/prompts.js.map +1 -1
- package/lib/commonjs/lib/qpqConfigs.d.ts +1 -0
- package/lib/commonjs/lib/qpqConfigs.js +9 -1
- package/lib/commonjs/lib/qpqConfigs.js.map +1 -1
- package/lib/commonjs/lib/runTasks.d.ts +5 -0
- package/lib/commonjs/lib/runTasks.js +82 -1
- package/lib/commonjs/lib/runTasks.js.map +1 -1
- package/lib/commonjs/lib/typeWatcher.d.ts +2 -0
- package/lib/commonjs/lib/typeWatcher.js +59 -0
- package/lib/commonjs/lib/typeWatcher.js.map +1 -0
- package/lib/commonjs/platforms/aws/cdkApp.js +5 -1
- package/lib/commonjs/platforms/aws/cdkApp.js.map +1 -1
- package/lib/commonjs/platforms/aws/clearResources.d.ts +2 -0
- package/lib/commonjs/platforms/aws/clearResources.js +167 -0
- package/lib/commonjs/platforms/aws/clearResources.js.map +1 -0
- package/lib/commonjs/platforms/aws/go.js +15 -1
- package/lib/commonjs/platforms/aws/go.js.map +1 -1
- package/lib/commonjs/platforms/aws/goDocker.js +76 -8
- package/lib/commonjs/platforms/aws/goDocker.js.map +1 -1
- package/lib/commonjs/platforms/aws/index.js +2 -0
- package/lib/commonjs/platforms/aws/index.js.map +1 -1
- package/lib/commonjs/platforms/docker/go.js +2 -2
- package/lib/commonjs/platforms/docker/go.js.map +1 -1
- package/lib/commonjs/platforms/types.d.ts +2 -0
- package/lib/commonjs/platforms/types.js.map +1 -1
- package/lib/esm/cli/commandMenu.d.ts +2 -0
- package/lib/esm/cli/commandMenu.js +67 -0
- package/lib/esm/cli/commandMenu.js.map +1 -0
- package/lib/esm/cli/commandRegistry.d.ts +10 -0
- package/lib/esm/cli/commandRegistry.js +115 -0
- package/lib/esm/cli/commandRegistry.js.map +1 -0
- package/lib/esm/cli/runCli.js +49 -51
- package/lib/esm/cli/runCli.js.map +1 -1
- package/lib/esm/commands/clearResources.d.ts +1 -0
- package/lib/esm/commands/clearResources.js +23 -0
- package/lib/esm/commands/clearResources.js.map +1 -0
- package/lib/esm/commands/go.js +3 -2
- package/lib/esm/commands/go.js.map +1 -1
- package/lib/esm/commands/goDev.js +11 -2
- package/lib/esm/commands/goDev.js.map +1 -1
- package/lib/esm/commands/goDevApi.js +7 -40
- package/lib/esm/commands/goDevApi.js.map +1 -1
- package/lib/esm/commands/goDevWeb.js +2 -0
- package/lib/esm/commands/goDevWeb.js.map +1 -1
- package/lib/esm/commands/goDocker.js +3 -2
- package/lib/esm/commands/goDocker.js.map +1 -1
- package/lib/esm/commands/hooks.js +21 -6
- package/lib/esm/commands/hooks.js.map +1 -1
- package/lib/esm/lib/clearResourcesPlan.d.ts +15 -0
- package/lib/esm/lib/clearResourcesPlan.js +66 -0
- package/lib/esm/lib/clearResourcesPlan.js.map +1 -0
- package/lib/esm/lib/deployPrompts.d.ts +1 -0
- package/lib/esm/lib/deployPrompts.js +38 -1
- package/lib/esm/lib/deployPrompts.js.map +1 -1
- package/lib/esm/lib/devServerEntry.js +3 -0
- package/lib/esm/lib/devServerEntry.js.map +1 -1
- package/lib/esm/lib/hooks.d.ts +2 -1
- package/lib/esm/lib/hooks.js +97 -5
- package/lib/esm/lib/hooks.js.map +1 -1
- package/lib/esm/lib/killStaleListeners.d.ts +3 -0
- package/lib/esm/lib/killStaleListeners.js +161 -0
- package/lib/esm/lib/killStaleListeners.js.map +1 -0
- package/lib/esm/lib/prompts.d.ts +10 -0
- package/lib/esm/lib/prompts.js +12 -0
- package/lib/esm/lib/prompts.js.map +1 -1
- package/lib/esm/lib/qpqConfigs.d.ts +1 -0
- package/lib/esm/lib/qpqConfigs.js +7 -0
- package/lib/esm/lib/qpqConfigs.js.map +1 -1
- package/lib/esm/lib/runTasks.d.ts +5 -0
- package/lib/esm/lib/runTasks.js +80 -0
- package/lib/esm/lib/runTasks.js.map +1 -1
- package/lib/esm/lib/typeWatcher.d.ts +2 -0
- package/lib/esm/lib/typeWatcher.js +51 -0
- package/lib/esm/lib/typeWatcher.js.map +1 -0
- package/lib/esm/platforms/aws/cdkApp.js +5 -1
- package/lib/esm/platforms/aws/cdkApp.js.map +1 -1
- package/lib/esm/platforms/aws/clearResources.d.ts +2 -0
- package/lib/esm/platforms/aws/clearResources.js +151 -0
- package/lib/esm/platforms/aws/clearResources.js.map +1 -0
- package/lib/esm/platforms/aws/go.js +16 -2
- package/lib/esm/platforms/aws/go.js.map +1 -1
- package/lib/esm/platforms/aws/goDocker.js +77 -9
- package/lib/esm/platforms/aws/goDocker.js.map +1 -1
- package/lib/esm/platforms/aws/index.js +2 -0
- package/lib/esm/platforms/aws/index.js.map +1 -1
- package/lib/esm/platforms/docker/go.js +2 -2
- package/lib/esm/platforms/docker/go.js.map +1 -1
- package/lib/esm/platforms/types.d.ts +2 -0
- package/lib/esm/platforms/types.js.map +1 -1
- package/package.json +12 -10
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The question flow for `qpq clear-resources`: enumerate every service's OWNED
|
|
2
|
+
// data resources (storage drives -> buckets, key value stores -> tables) from
|
|
3
|
+
// its live qpq config, multi-select which to empty, then type the deploy
|
|
4
|
+
// target back to confirm - emptying deletes stored data, so a y/n prompt
|
|
5
|
+
// isn't enough friction. Owned-only so a cross-module ref (e.g. a membership
|
|
6
|
+
// table every service declares against one owner) appears exactly once, on
|
|
7
|
+
// the service that owns it.
|
|
8
|
+
import { qpqCoreUtils } from 'quidproquo-core';
|
|
9
|
+
import { getServiceNames } from './discovery';
|
|
10
|
+
import { promptCheckbox, promptSelect, promptText } from './prompts';
|
|
11
|
+
import { loadServiceQpqConfig } from './qpqConfigs';
|
|
12
|
+
// Every owned data resource across the app's services, in service order.
|
|
13
|
+
export const getClearableResources = (appName) => {
|
|
14
|
+
const resources = [];
|
|
15
|
+
for (const service of getServiceNames(appName)) {
|
|
16
|
+
const qpqConfig = loadServiceQpqConfig(appName, service);
|
|
17
|
+
for (const drive of qpqCoreUtils.getOwnedStorageDrives(qpqConfig)) {
|
|
18
|
+
resources.push({ service, kind: 'storageDrive', resourceName: drive.storageDrive });
|
|
19
|
+
}
|
|
20
|
+
for (const store of qpqCoreUtils.getOwnedKeyValueStores(qpqConfig)) {
|
|
21
|
+
resources.push({ service, kind: 'keyValueStore', resourceName: store.keyValueStoreName });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return resources;
|
|
25
|
+
};
|
|
26
|
+
const BY_SERVICE = 'By service — empty everything the selected services own';
|
|
27
|
+
const BY_RESOURCE = 'By resource — pick individual buckets/tables';
|
|
28
|
+
// Service mode: pick services, take every resource each one owns.
|
|
29
|
+
const pickByService = async (clearable) => {
|
|
30
|
+
const services = [...new Set(clearable.map((resource) => resource.service))];
|
|
31
|
+
const countFor = (service) => clearable.filter((resource) => resource.service === service).length;
|
|
32
|
+
const picked = await promptCheckbox('Select services to EMPTY (every bucket/table the service owns)', services.map((service) => ({ name: `${service} (${countFor(service)} resources)`, value: service })));
|
|
33
|
+
return clearable.filter((resource) => picked.includes(resource.service));
|
|
34
|
+
};
|
|
35
|
+
// Resource mode: pick individual buckets/tables.
|
|
36
|
+
const pickByResource = (clearable) => promptCheckbox('Select resources to EMPTY (all stored data will be deleted)', clearable.map((resource) => ({
|
|
37
|
+
name: `${resource.service} — ${resource.resourceName} (${resource.kind === 'storageDrive' ? 'bucket' : 'table'})`,
|
|
38
|
+
value: resource,
|
|
39
|
+
})));
|
|
40
|
+
export const promptClearResourcesPlan = async (appName) => {
|
|
41
|
+
const deployInfo = process.env.ACTOR_NAME
|
|
42
|
+
? `${appName}-${process.env.ENVIRONMENT}-${process.env.ACTOR_NAME}`
|
|
43
|
+
: `${appName}-${process.env.ENVIRONMENT}`;
|
|
44
|
+
const clearable = getClearableResources(appName);
|
|
45
|
+
if (clearable.length === 0) {
|
|
46
|
+
console.log('No storage drives or key value stores found in this app.');
|
|
47
|
+
return { kind: 'cancelled' };
|
|
48
|
+
}
|
|
49
|
+
const mode = await promptSelect('How do you want to select what to empty?', [BY_SERVICE, BY_RESOURCE]);
|
|
50
|
+
const resources = mode === BY_SERVICE ? await pickByService(clearable) : await pickByResource(clearable);
|
|
51
|
+
if (resources.length === 0) {
|
|
52
|
+
console.log('Nothing selected. Exiting!');
|
|
53
|
+
return { kind: 'cancelled' };
|
|
54
|
+
}
|
|
55
|
+
console.log(`\nThis will EMPTY (delete all stored data from) ${resources.length} resource(s) in [${deployInfo}]:`);
|
|
56
|
+
for (const resource of resources) {
|
|
57
|
+
console.log(` ${resource.service} — ${resource.resourceName} (${resource.kind === 'storageDrive' ? 'bucket' : 'table'})`);
|
|
58
|
+
}
|
|
59
|
+
const confirmation = await promptText(`Type '${deployInfo}' to confirm`);
|
|
60
|
+
if (confirmation !== deployInfo) {
|
|
61
|
+
console.log('Confirmation did not match. Exiting!');
|
|
62
|
+
return { kind: 'cancelled' };
|
|
63
|
+
}
|
|
64
|
+
return { kind: 'resources', appName, resources };
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=clearResourcesPlan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clearResourcesPlan.js","sourceRoot":"","sources":["../../../src/lib/clearResourcesPlan.ts"],"names":[],"mappings":"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","sourcesContent":["// The question flow for `qpq clear-resources`: enumerate every service's OWNED\n// data resources (storage drives -> buckets, key value stores -> tables) from\n// its live qpq config, multi-select which to empty, then type the deploy\n// target back to confirm - emptying deletes stored data, so a y/n prompt\n// isn't enough friction. Owned-only so a cross-module ref (e.g. a membership\n// table every service declares against one owner) appears exactly once, on\n// the service that owns it.\nimport { qpqCoreUtils } from 'quidproquo-core';\n\nimport { getServiceNames } from './discovery';\nimport { promptCheckbox, promptSelect, promptText } from './prompts';\nimport { loadServiceQpqConfig } from './qpqConfigs';\n\nexport type ClearableResourceKind = 'storageDrive' | 'keyValueStore';\n\nexport type ClearableResource = {\n service: string;\n kind: ClearableResourceKind;\n // The config-level resource name (e.g. 'templateTestAssets', 'userTenantLinks');\n // the platform driver resolves it to the concrete runtime name (bucket/table).\n resourceName: string;\n};\n\nexport type ClearResourcesPlan =\n | { kind: 'cancelled' }\n // Empty (not destroy) the selected resources' stored data.\n | { kind: 'resources'; appName: string; resources: ClearableResource[] };\n\n// Every owned data resource across the app's services, in service order.\nexport const getClearableResources = (appName: string): ClearableResource[] => {\n const resources: ClearableResource[] = [];\n\n for (const service of getServiceNames(appName)) {\n const qpqConfig = loadServiceQpqConfig(appName, service);\n\n for (const drive of qpqCoreUtils.getOwnedStorageDrives(qpqConfig)) {\n resources.push({ service, kind: 'storageDrive', resourceName: drive.storageDrive });\n }\n\n for (const store of qpqCoreUtils.getOwnedKeyValueStores(qpqConfig)) {\n resources.push({ service, kind: 'keyValueStore', resourceName: store.keyValueStoreName });\n }\n }\n\n return resources;\n};\n\nconst BY_SERVICE = 'By service — empty everything the selected services own';\nconst BY_RESOURCE = 'By resource — pick individual buckets/tables';\n\n// Service mode: pick services, take every resource each one owns.\nconst pickByService = async (clearable: ClearableResource[]): Promise<ClearableResource[]> => {\n const services = [...new Set(clearable.map((resource) => resource.service))];\n\n const countFor = (service: string) => clearable.filter((resource) => resource.service === service).length;\n\n const picked = await promptCheckbox(\n 'Select services to EMPTY (every bucket/table the service owns)',\n services.map((service) => ({ name: `${service} (${countFor(service)} resources)`, value: service })),\n );\n\n return clearable.filter((resource) => picked.includes(resource.service));\n};\n\n// Resource mode: pick individual buckets/tables.\nconst pickByResource = (clearable: ClearableResource[]): Promise<ClearableResource[]> =>\n promptCheckbox(\n 'Select resources to EMPTY (all stored data will be deleted)',\n clearable.map((resource) => ({\n name: `${resource.service} — ${resource.resourceName} (${resource.kind === 'storageDrive' ? 'bucket' : 'table'})`,\n value: resource,\n })),\n );\n\nexport const promptClearResourcesPlan = async (appName: string): Promise<ClearResourcesPlan> => {\n const deployInfo = process.env.ACTOR_NAME\n ? `${appName}-${process.env.ENVIRONMENT}-${process.env.ACTOR_NAME}`\n : `${appName}-${process.env.ENVIRONMENT}`;\n\n const clearable = getClearableResources(appName);\n\n if (clearable.length === 0) {\n console.log('No storage drives or key value stores found in this app.');\n return { kind: 'cancelled' };\n }\n\n const mode = await promptSelect('How do you want to select what to empty?', [BY_SERVICE, BY_RESOURCE]);\n\n const resources = mode === BY_SERVICE ? await pickByService(clearable) : await pickByResource(clearable);\n\n if (resources.length === 0) {\n console.log('Nothing selected. Exiting!');\n return { kind: 'cancelled' };\n }\n\n console.log(`\\nThis will EMPTY (delete all stored data from) ${resources.length} resource(s) in [${deployInfo}]:`);\n for (const resource of resources) {\n console.log(` ${resource.service} — ${resource.resourceName} (${resource.kind === 'storageDrive' ? 'bucket' : 'table'})`);\n }\n\n const confirmation = await promptText(`Type '${deployInfo}' to confirm`);\n\n if (confirmation !== deployInfo) {\n console.log('Confirmation did not match. Exiting!');\n return { kind: 'cancelled' };\n }\n\n return { kind: 'resources', appName, resources };\n};\n"]}
|
|
@@ -14,6 +14,7 @@ export type DeployPlan = {
|
|
|
14
14
|
services: string[];
|
|
15
15
|
stacks: StackType;
|
|
16
16
|
};
|
|
17
|
+
export declare const buildDeployPlanFromArgs: (appName: string, argv: string[]) => DeployPlan | null;
|
|
17
18
|
export declare const promptDeployPlan: (appName: string) => Promise<DeployPlan>;
|
|
18
19
|
export type TeardownPlan = {
|
|
19
20
|
kind: 'cancelled';
|
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
// The interactive question flow shared by `qpq go` (sequential AWS deploy) and
|
|
2
2
|
// a future dockerized variant: pick an app, confirm the target, pick services
|
|
3
|
-
// (or domain/bootstrap), pick stacks.
|
|
3
|
+
// (or domain/bootstrap), pick stacks. buildDeployPlanFromArgs is the
|
|
4
|
+
// non-interactive shortcut (`qpq go all all`) that skips every prompt.
|
|
5
|
+
import { getPositionalArgs } from './args';
|
|
4
6
|
import { getServiceNames } from './discovery';
|
|
5
7
|
import { promptSelect, promptText, promptYesNo } from './prompts';
|
|
8
|
+
const STACK_TYPES = ['all', 'inf', 'api', 'web', 'views'];
|
|
9
|
+
// Non-interactive deploy plan from positional args: `qpq go <services> [stacks]`.
|
|
10
|
+
// services: 'all' | comma-separated service names | 'account' | 'domain' | 'bootstrap'
|
|
11
|
+
// stacks: all | inf | api | web | views (defaults to 'all')
|
|
12
|
+
// Returns null when no positional services token is given, so callers fall back
|
|
13
|
+
// to the interactive promptDeployPlan. Passing args is an explicit opt-in to a
|
|
14
|
+
// prompt-free deploy, so the "Deploying to: [env]" confirmation is skipped too.
|
|
15
|
+
export const buildDeployPlanFromArgs = (appName, argv) => {
|
|
16
|
+
const [servicesArg, stacksArg] = getPositionalArgs(argv, ['--app', '--env', '--platform']);
|
|
17
|
+
if (!servicesArg) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (servicesArg === 'account') {
|
|
21
|
+
return { kind: 'account', appName };
|
|
22
|
+
}
|
|
23
|
+
if (servicesArg === 'domain') {
|
|
24
|
+
return { kind: 'bootstrap', appName, includeDomain: true };
|
|
25
|
+
}
|
|
26
|
+
if (servicesArg === 'bootstrap') {
|
|
27
|
+
return { kind: 'bootstrap', appName, includeDomain: false };
|
|
28
|
+
}
|
|
29
|
+
const allServices = getServiceNames(appName);
|
|
30
|
+
const services = servicesArg === 'all' ? allServices : servicesArg.split(',').map((s) => s.trim()).filter(Boolean);
|
|
31
|
+
const unknownServices = services.filter((service) => !allServices.includes(service));
|
|
32
|
+
if (unknownServices.length > 0) {
|
|
33
|
+
console.error(`Unknown service(s): ${unknownServices.join(', ')}. Valid services: ${allServices.join(', ')}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const stacks = (stacksArg ?? 'all');
|
|
37
|
+
if (!STACK_TYPES.includes(stacks)) {
|
|
38
|
+
console.error(`Unknown stacks '${stacksArg}'. Valid stacks: ${STACK_TYPES.join(', ')}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
return { kind: 'services', appName, services: [...new Set(services)], stacks };
|
|
42
|
+
};
|
|
6
43
|
export const promptDeployPlan = async (appName) => {
|
|
7
44
|
const deployInfo = process.env.ACTOR_NAME
|
|
8
45
|
? `${appName}-${process.env.ENVIRONMENT}-${process.env.ACTOR_NAME}`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployPrompts.js","sourceRoot":"","sources":["../../../src/lib/deployPrompts.ts"],"names":[],"mappings":"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","sourcesContent":["// The interactive question flow shared by `qpq go` (sequential AWS deploy) and\n// a future dockerized variant: pick an app, confirm the target, pick services\n// (or domain/bootstrap), pick stacks.\nimport { getServiceNames } from './discovery';\nimport { promptSelect, promptText, promptYesNo } from './prompts';\n\nexport type StackType = 'all' | 'inf' | 'api' | 'web' | 'views';\n\nexport type DeployPlan =\n | { kind: 'cancelled' }\n // Deploy only the account-level guardrails stack (one per AWS account).\n | { kind: 'account'; appName: string }\n // Deploy the bootstrap stack (and optionally the domain stack first).\n | { kind: 'bootstrap'; appName: string; includeDomain: boolean }\n // Deploy the selected stacks for the selected services.\n | {\n kind: 'services';\n appName: string;\n services: string[];\n stacks: StackType;\n };\n\nexport const promptDeployPlan = async (appName: string): Promise<DeployPlan> => {\n const deployInfo = process.env.ACTOR_NAME\n ? `${appName}-${process.env.ENVIRONMENT}-${process.env.ACTOR_NAME}`\n : `${appName}-${process.env.ENVIRONMENT}`;\n\n if (!(await promptYesNo(`Deploying to: [${deployInfo}]`))) {\n console.log('Exiting!');\n return { kind: 'cancelled' };\n }\n\n let services: string[] = [];\n\n do {\n const allServices = getServiceNames(appName);\n\n // Bracketed so the meta options can't be mistaken for actual services.\n const allText = '[ all ]';\n const accountText = '[ account ]';\n const domainEnvText = '[ domain ]';\n const bootstrapEnvText = '[ bootstrap environment ]';\n\n const service = await promptSelect('Select services to deploy', [allText, ...allServices, accountText, domainEnvText, bootstrapEnvText]);\n\n if (service === allText) {\n services = allServices;\n break;\n }\n if (service === accountText) {\n return { kind: 'account', appName };\n }\n if (service === domainEnvText) {\n return { kind: 'bootstrap', appName, includeDomain: true };\n }\n if (service === bootstrapEnvText) {\n return { kind: 'bootstrap', appName, includeDomain: false };\n }\n\n services.push(service);\n } while (await promptYesNo('Do you want to deploy another service?'));\n\n services = [...new Set(services)];\n\n const stacks = (await promptSelect('Stacks to deploy', ['all', 'inf', 'api', 'web', 'views'])) as StackType;\n\n return { kind: 'services', appName, services, stacks };\n};\n\nexport type TeardownPlan =\n | { kind: 'cancelled' }\n // Destroy the web, api and inf stacks for the selected services.\n | { kind: 'services'; appName: string; services: string[] };\n\n// The question flow for `qpq teardown`: pick services, then type the deploy\n// target back to confirm — destroying inf stacks takes user data (key value\n// stores, user directories) with it, so a y/n prompt isn't enough friction.\nexport const promptTeardownPlan = async (appName: string): Promise<TeardownPlan> => {\n const deployInfo = process.env.ACTOR_NAME\n ? `${appName}-${process.env.ENVIRONMENT}-${process.env.ACTOR_NAME}`\n : `${appName}-${process.env.ENVIRONMENT}`;\n\n let services: string[] = [];\n\n do {\n const allServices = getServiceNames(appName);\n const allText = '[ all ]';\n\n const service = await promptSelect('Select services to tear down', [allText, ...allServices]);\n\n if (service === allText) {\n services = allServices;\n break;\n }\n\n services.push(service);\n } while (await promptYesNo('Do you want to tear down another service?'));\n\n services = [...new Set(services)];\n\n console.log(`\\nThis will DESTROY the web, api and inf stacks (including all stored data) for: ${services.join(', ')}`);\n const confirmation = await promptText(`Type '${deployInfo}' to confirm`);\n\n if (confirmation !== deployInfo) {\n console.log('Confirmation did not match. Exiting!');\n return { kind: 'cancelled' };\n }\n\n return { kind: 'services', appName, services };\n};\n"]}
|
|
1
|
+
{"version":3,"file":"deployPrompts.js","sourceRoot":"","sources":["../../../src/lib/deployPrompts.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8EAA8E;AAC9E,qEAAqE;AACrE,uEAAuE;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAIlE,MAAM,WAAW,GAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AAgBvE,kFAAkF;AAClF,yFAAyF;AACzF,yEAAyE;AACzE,gFAAgF;AAChF,+EAA+E;AAC/E,gFAAgF;AAChF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,OAAe,EAAE,IAAc,EAAqB,EAAE;IAC5F,MAAM,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG,iBAAiB,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;IAE3F,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;IACtC,CAAC;IACD,IAAI,WAAW,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;IAC7D,CAAC;IACD,IAAI,WAAW,KAAK,WAAW,EAAE,CAAC;QAChC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;IAC9D,CAAC;IAED,MAAM,WAAW,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,QAAQ,GAAG,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAEnH,MAAM,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IACrF,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,uBAAuB,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,qBAAqB,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9G,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,SAAS,IAAI,KAAK,CAAc,CAAC;IACjD,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAClC,OAAO,CAAC,KAAK,CAAC,mBAAmB,SAAS,oBAAoB,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;AACjF,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,OAAe,EAAuB,EAAE;IAC7E,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU;QACvC,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE;QACnE,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;IAE5C,IAAI,CAAC,CAAC,MAAM,WAAW,CAAC,kBAAkB,UAAU,GAAG,CAAC,CAAC,EAAE,CAAC;QAC1D,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACxB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC/B,CAAC;IAED,IAAI,QAAQ,GAAa,EAAE,CAAC;IAE5B,GAAG,CAAC;QACF,MAAM,WAAW,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QAE7C,uEAAuE;QACvE,MAAM,OAAO,GAAG,SAAS,CAAC;QAC1B,MAAM,WAAW,GAAG,aAAa,CAAC;QAClC,MAAM,aAAa,GAAG,YAAY,CAAC;QACnC,MAAM,gBAAgB,GAAG,2BAA2B,CAAC;QAErD,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,2BAA2B,EAAE,CAAC,OAAO,EAAE,GAAG,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,CAAC,CAAC,CAAC;QAEzI,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YACxB,QAAQ,GAAG,WAAW,CAAC;YACvB,MAAM;QACR,CAAC;QACD,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;YAC5B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;QACtC,CAAC;QACD,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC;YAC9B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;QAC7D,CAAC;QACD,IAAI,OAAO,KAAK,gBAAgB,EAAE,CAAC;YACjC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;QAC9D,CAAC;QAED,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC,QAAQ,MAAM,WAAW,CAAC,wCAAwC,CAAC,EAAE;IAEtE,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;IAElC,MAAM,MAAM,GAAG,CAAC,MAAM,YAAY,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAc,CAAC;IAE5G,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AACzD,CAAC,CAAC;AAOF,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EAAE,OAAe,EAAyB,EAAE;IACjF,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU;QACvC,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE;QACnE,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;IAE5C,IAAI,QAAQ,GAAa,EAAE,CAAC;IAE5B,GAAG,CAAC;QACF,MAAM,WAAW,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,SAAS,CAAC;QAE1B,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,8BAA8B,EAAE,CAAC,OAAO,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC;QAE9F,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YACxB,QAAQ,GAAG,WAAW,CAAC;YACvB,MAAM;QACR,CAAC;QAED,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC,QAAQ,MAAM,WAAW,CAAC,2CAA2C,CAAC,EAAE;IAEzE,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;IAElC,OAAO,CAAC,GAAG,CAAC,oFAAoF,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvH,MAAM,YAAY,GAAG,MAAM,UAAU,CAAC,SAAS,UAAU,cAAc,CAAC,CAAC;IAEzE,IAAI,YAAY,KAAK,UAAU,EAAE,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;QACpD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC/B,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AACjD,CAAC,CAAC","sourcesContent":["// The interactive question flow shared by `qpq go` (sequential AWS deploy) and\n// a future dockerized variant: pick an app, confirm the target, pick services\n// (or domain/bootstrap), pick stacks. buildDeployPlanFromArgs is the\n// non-interactive shortcut (`qpq go all all`) that skips every prompt.\nimport { getPositionalArgs } from './args';\nimport { getServiceNames } from './discovery';\nimport { promptSelect, promptText, promptYesNo } from './prompts';\n\nexport type StackType = 'all' | 'inf' | 'api' | 'web' | 'views';\n\nconst STACK_TYPES: StackType[] = ['all', 'inf', 'api', 'web', 'views'];\n\nexport type DeployPlan =\n | { kind: 'cancelled' }\n // Deploy only the account-level guardrails stack (one per AWS account).\n | { kind: 'account'; appName: string }\n // Deploy the bootstrap stack (and optionally the domain stack first).\n | { kind: 'bootstrap'; appName: string; includeDomain: boolean }\n // Deploy the selected stacks for the selected services.\n | {\n kind: 'services';\n appName: string;\n services: string[];\n stacks: StackType;\n };\n\n// Non-interactive deploy plan from positional args: `qpq go <services> [stacks]`.\n// services: 'all' | comma-separated service names | 'account' | 'domain' | 'bootstrap'\n// stacks: all | inf | api | web | views (defaults to 'all')\n// Returns null when no positional services token is given, so callers fall back\n// to the interactive promptDeployPlan. Passing args is an explicit opt-in to a\n// prompt-free deploy, so the \"Deploying to: [env]\" confirmation is skipped too.\nexport const buildDeployPlanFromArgs = (appName: string, argv: string[]): DeployPlan | null => {\n const [servicesArg, stacksArg] = getPositionalArgs(argv, ['--app', '--env', '--platform']);\n\n if (!servicesArg) {\n return null;\n }\n\n if (servicesArg === 'account') {\n return { kind: 'account', appName };\n }\n if (servicesArg === 'domain') {\n return { kind: 'bootstrap', appName, includeDomain: true };\n }\n if (servicesArg === 'bootstrap') {\n return { kind: 'bootstrap', appName, includeDomain: false };\n }\n\n const allServices = getServiceNames(appName);\n const services = servicesArg === 'all' ? allServices : servicesArg.split(',').map((s) => s.trim()).filter(Boolean);\n\n const unknownServices = services.filter((service) => !allServices.includes(service));\n if (unknownServices.length > 0) {\n console.error(`Unknown service(s): ${unknownServices.join(', ')}. Valid services: ${allServices.join(', ')}`);\n process.exit(1);\n }\n\n const stacks = (stacksArg ?? 'all') as StackType;\n if (!STACK_TYPES.includes(stacks)) {\n console.error(`Unknown stacks '${stacksArg}'. Valid stacks: ${STACK_TYPES.join(', ')}`);\n process.exit(1);\n }\n\n return { kind: 'services', appName, services: [...new Set(services)], stacks };\n};\n\nexport const promptDeployPlan = async (appName: string): Promise<DeployPlan> => {\n const deployInfo = process.env.ACTOR_NAME\n ? `${appName}-${process.env.ENVIRONMENT}-${process.env.ACTOR_NAME}`\n : `${appName}-${process.env.ENVIRONMENT}`;\n\n if (!(await promptYesNo(`Deploying to: [${deployInfo}]`))) {\n console.log('Exiting!');\n return { kind: 'cancelled' };\n }\n\n let services: string[] = [];\n\n do {\n const allServices = getServiceNames(appName);\n\n // Bracketed so the meta options can't be mistaken for actual services.\n const allText = '[ all ]';\n const accountText = '[ account ]';\n const domainEnvText = '[ domain ]';\n const bootstrapEnvText = '[ bootstrap environment ]';\n\n const service = await promptSelect('Select services to deploy', [allText, ...allServices, accountText, domainEnvText, bootstrapEnvText]);\n\n if (service === allText) {\n services = allServices;\n break;\n }\n if (service === accountText) {\n return { kind: 'account', appName };\n }\n if (service === domainEnvText) {\n return { kind: 'bootstrap', appName, includeDomain: true };\n }\n if (service === bootstrapEnvText) {\n return { kind: 'bootstrap', appName, includeDomain: false };\n }\n\n services.push(service);\n } while (await promptYesNo('Do you want to deploy another service?'));\n\n services = [...new Set(services)];\n\n const stacks = (await promptSelect('Stacks to deploy', ['all', 'inf', 'api', 'web', 'views'])) as StackType;\n\n return { kind: 'services', appName, services, stacks };\n};\n\nexport type TeardownPlan =\n | { kind: 'cancelled' }\n // Destroy the web, api and inf stacks for the selected services.\n | { kind: 'services'; appName: string; services: string[] };\n\n// The question flow for `qpq teardown`: pick services, then type the deploy\n// target back to confirm — destroying inf stacks takes user data (key value\n// stores, user directories) with it, so a y/n prompt isn't enough friction.\nexport const promptTeardownPlan = async (appName: string): Promise<TeardownPlan> => {\n const deployInfo = process.env.ACTOR_NAME\n ? `${appName}-${process.env.ENVIRONMENT}-${process.env.ACTOR_NAME}`\n : `${appName}-${process.env.ENVIRONMENT}`;\n\n let services: string[] = [];\n\n do {\n const allServices = getServiceNames(appName);\n const allText = '[ all ]';\n\n const service = await promptSelect('Select services to tear down', [allText, ...allServices]);\n\n if (service === allText) {\n services = allServices;\n break;\n }\n\n services.push(service);\n } while (await promptYesNo('Do you want to tear down another service?'));\n\n services = [...new Set(services)];\n\n console.log(`\\nThis will DESTROY the web, api and inf stacks (including all stored data) for: ${services.join(', ')}`);\n const confirmation = await promptText(`Type '${deployInfo}' to confirm`);\n\n if (confirmation !== deployInfo) {\n console.log('Confirmation did not match. Exiting!');\n return { kind: 'cancelled' };\n }\n\n return { kind: 'services', appName, services };\n};\n"]}
|
|
@@ -29,6 +29,9 @@ startDevServer(
|
|
|
29
29
|
serverDomain: 'localhost',
|
|
30
30
|
serverPort: 8080,
|
|
31
31
|
webSocketPort: 8888,
|
|
32
|
+
// Scoped per app so running the dev server for two apps out of the same
|
|
33
|
+
// repo root doesn't share kvs/file-storage/config state.
|
|
34
|
+
runtimePath: ${JSON.stringify(path.join('.qpq-runtime', appName))},
|
|
32
35
|
fileStorageConfig: {
|
|
33
36
|
secureUrlSecret: 'dev-secure-url-secret',
|
|
34
37
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devServerEntry.js","sourceRoot":"","sources":["../../../src/lib/devServerEntry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,6EAA6E;AAC7E,8EAA8E;AAC9E,yEAAyE;AACzE,4EAA4E;AAC5E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,IAAY,EAAE,OAAe,EAAU,EAAE;IAC3E,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAElD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC,CAAC;IACjF,MAAM,eAAe,GAAG,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC;QAClD,CAAC,CAAC,wCAAwC,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,GAAG;QAC/F,CAAC,CAAC,sCAAsC,CAAC;IAE3C,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,EAAE,CAAC,aAAa,CACd,SAAS,EACT;;;;;;;;EAQF,eAAe
|
|
1
|
+
{"version":3,"file":"devServerEntry.js","sourceRoot":"","sources":["../../../src/lib/devServerEntry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,6EAA6E;AAC7E,8EAA8E;AAC9E,yEAAyE;AACzE,4EAA4E;AAC5E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,IAAY,EAAE,OAAe,EAAU,EAAE;IAC3E,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAElD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC,CAAC;IACjF,MAAM,eAAe,GAAG,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC;QAClD,CAAC,CAAC,wCAAwC,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,GAAG;QAC/F,CAAC,CAAC,sCAAsC,CAAC;IAE3C,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,EAAE,CAAC,aAAa,CACd,SAAS,EACT;;;;;;;;EAQF,eAAe;;;;;;;;;mBASE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;;;;;;;;;;;;CAYpE,CACE,CAAC;IAEF,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC","sourcesContent":["import fs from 'fs';\nimport path from 'path';\n\nimport { getAppDirectory } from './discovery';\n\n// The dev-server bootstrap, generated per build so it can import an optional\n// app-owned overrides file (apps/<app>/devServer.config.ts, default-exporting\n// DevServerConfigOverrides). The `quidproquo-dynamic-loader` import is a\n// virtual module injected by QpqPlugin at bundle time. QPQ_WEB_ROOT (set in\n// the docker platform image) turns on static views serving.\nexport const writeDevServerEntry = (root: string, appName: string): string => {\n const entryDir = path.join(root, 'dist', 'qpq', 'dev-server');\n const entryPath = path.join(entryDir, 'entry.ts');\n\n const overridesPath = path.join(getAppDirectory(appName), 'devServer.config.ts');\n const overridesImport = fs.existsSync(overridesPath)\n ? `import devServerConfigOverrides from ${JSON.stringify(overridesPath.replace(/\\.ts$/, ''))};`\n : 'const devServerConfigOverrides = {};';\n\n fs.mkdirSync(entryDir, { recursive: true });\n fs.writeFileSync(\n entryPath,\n `// GENERATED by qpq — do not edit.\nimport { startDevServer } from 'quidproquo-dev-server';\nimport {\n qpqConfigs,\n qpqDynamicModuleLoaderForService,\n // @ts-ignore - Special build-time virtual module\n} from 'quidproquo-dynamic-loader';\n\n${overridesImport}\n\nstartDevServer(\n {\n serverDomain: 'localhost',\n serverPort: 8080,\n webSocketPort: 8888,\n // Scoped per app so running the dev server for two apps out of the same\n // repo root doesn't share kvs/file-storage/config state.\n runtimePath: ${JSON.stringify(path.join('.qpq-runtime', appName))},\n fileStorageConfig: {\n secureUrlSecret: 'dev-secure-url-secret',\n },\n qpqConfigs,\n dynamicModuleLoader: qpqDynamicModuleLoaderForService,\n webRoot: process.env.QPQ_WEB_ROOT,\n logServiceName: 'admin',\n delay: { default: 0 },\n },\n devServerConfigOverrides,\n);\n`,\n );\n\n return entryPath;\n};\n"]}
|
package/lib/esm/lib/hooks.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import { TaskFailure } from './runTasks';
|
|
1
2
|
export declare const runAppHook: (appName: string, hook: string) => Promise<boolean>;
|
|
2
|
-
export declare const
|
|
3
|
+
export declare const runHookForAllPackages: (hook: string, jobs: number) => Promise<TaskFailure[]>;
|
package/lib/esm/lib/hooks.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
package/lib/esm/lib/hooks.js.map
CHANGED
|
@@ -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,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":"AAAA,0EAA0E;AAC1E,8EAA8E;AAC9E,wEAAwE;AACxE,0EAA0E;AAC1E,0EAA0E;AAC1E,gCAAgC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,yEAAyE;AACzE,6EAA6E;AAC7E,yEAAyE;AACzE,6EAA6E;AAC7E,mEAAmE;AACnE,4DAA4D;AAC5D,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAe,EAAW,EAAE,CAC1D,OAAO;KACJ,IAAI,EAAE;KACN,KAAK,CAAC,KAAK,CAAC;KACZ,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;IACd,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnE,OAAO,UAAU,KAAK,KAAK,CAAC;AAC9B,CAAC,CAAC,CAAC;AAEP,0EAA0E;AAC1E,2EAA2E;AAC3E,2EAA2E;AAC3E,sEAAsE;AACtE,6EAA6E;AAC7E,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,2EAA2E;AAC3E,4EAA4E;AAC5E,2EAA2E;AAC3E,+CAA+C;AAC/C,MAAM,cAAc,GAAG,CAAC,GAAW,EAAE,SAAS,GAAG,IAAI,EAAQ,EAAE;IAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IACxC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,uCAAuC;QACjD,CAAC;QACD,QAAQ,CAAC,YAAY,CAAC,CAAC;IACzB,CAAC;AACH,CAAC,CAAC;AAEF,2EAA2E;AAC3E,wEAAwE;AACxE,2EAA2E;AAC3E,oEAAoE;AACpE,MAAM,aAAa,GAAG,CAAC,GAAW,EAAiB,EAAE;IACnD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,GAAG,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;aAC3F,QAAQ,EAAE;aACV,KAAK,CAAC,IAAI,CAAC;aACX,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QAClC,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,sDAAsD;IACrE,CAAC;AACH,CAAC,CAAC;AAEF,6EAA6E;AAC7E,yEAAyE;AACzE,yEAAyE;AACzE,0EAA0E;AAC1E,2EAA2E;AAC3E,mCAAmC;AACnC,MAAM,kBAAkB,GAAG,GAAgB,EAAE;IAC3C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IACtB,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,CACV,QAAQ,CAAC,kBAAkB,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;iBACvE,QAAQ,EAAE;iBACV,IAAI,EAAE,CACV,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,MAAM;QACR,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC;AAEF,6EAA6E;AAC7E,6EAA6E;AAC7E,yEAAyE;AACzE,0EAA0E;AAC1E,2EAA2E;AAC3E,6EAA6E;AAC7E,wEAAwE;AACxE,0EAA0E;AAC1E,4EAA4E;AAC5E,uEAAuE;AACvE,0EAA0E;AAC1E,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,IAAY,EAAQ,EAAE;IAC7D,MAAM,YAAY,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IAErC,IAAI,KAAK,GAAa,EAAE,CAAC;IACzB,IAAI,CAAC;QACH,KAAK,GAAG,QAAQ,CAAC,uBAAuB,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;aAC/E,QAAQ,EAAE;aACV,KAAK,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO;YAAE,SAAS;QAEvB,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,UAAU,KAAK,CAAC,CAAC;YAAE,SAAS;QAEhC,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAErD,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC;YAAE,SAAS;QACpE,IAAI,aAAa,CAAC,GAAG,CAAC,KAAK,YAAY;YAAE,SAAS;QAElD,OAAO,CAAC,GAAG,CAAC,oDAAoD,GAAG,MAAM,OAAO,EAAE,CAAC,CAAC;QACpF,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC5B,cAAc,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,KAAe,EAAE,MAAoC,EAAQ,EAAE;IAChG,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,GAAa,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,IAAI,GAAG,QAAQ,CAAC,iBAAiB,IAAI,eAAe,EAAE;gBACpD,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;aACpC,CAAC;iBACC,QAAQ,EAAE;iBACV,KAAK,CAAC,IAAI,CAAC;iBACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;iBAClC,MAAM,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,SAAS,CAAC,gDAAgD;QAC5D,CAAC;QAED,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,OAAO,GAAG,EAAE,CAAC;YACjB,IAAI,CAAC;gBACH,OAAO,GAAG,QAAQ,CAAC,SAAS,GAAG,cAAc,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;YACnE,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS,CAAC,eAAe;YAC3B,CAAC;YACD,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;gBACpB,OAAO,CAAC,GAAG,CAAC,oCAAoC,IAAI,SAAS,GAAG,GAAG,CAAC,CAAC;gBACrE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAClB,cAAc,CAAC,GAAG,CAAC,CAAC;YACtB,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,2CAA2C,GAAG,KAAK,OAAO,qBAAqB,CAAC,CAAC;YAC5G,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC,CAAC","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"]}
|
package/lib/esm/lib/prompts.d.ts
CHANGED
|
@@ -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[]>;
|
package/lib/esm/lib/prompts.js
CHANGED
|
@@ -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"]}
|