@aws/nx-plugin 1.0.0-rc.67 → 1.0.0-rc.69
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/README.md +1 -0
- package/generators.json +7 -0
- package/migrations.json +6 -1
- package/package.json +1 -1
- package/src/agentcore-harness/__snapshots__/generator.spec.ts.snap +238 -0
- package/src/agentcore-harness/files/project/README.md.template +83 -0
- package/src/agentcore-harness/files/project/scripts/chat.ts.template +99 -0
- package/src/agentcore-harness/files/project/src/PROMPT.md.template +16 -0
- package/src/agentcore-harness/generator.d.ts +62 -0
- package/src/agentcore-harness/generator.js +158 -0
- package/src/agentcore-harness/generator.js.map +1 -0
- package/src/agentcore-harness/schema.d.js +8 -0
- package/src/agentcore-harness/schema.d.js.map +1 -0
- package/src/agentcore-harness/schema.d.ts +17 -0
- package/src/agentcore-harness/schema.json +53 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/build-system-prompt.ts.fixture +23 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/build-user-prompt.ts.fixture +96 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/config-path.ts.fixture +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/file-to-translate.ts.fixture +10 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/get-files-to-translate.ts.fixture +96 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/header.ts.fixture +15 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/log.ts.fixture +18 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/main-call.ts.fixture +6 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/main.ts.fixture +79 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/project-root.ts.fixture +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/run-with-concurrency.ts.fixture +57 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/scripts-dir.ts.fixture +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/translate-file-for-language.ts.fixture +61 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/already-migrated.grit +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/build-system-prompt.grit +6 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/build-user-prompt.grit +6 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/config-path.grit +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/file-to-translate.grit +11 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/get-files-to-translate.grit +7 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/log.grit +3 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/main-call.grit +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/main.grit +8 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/project-root.grit +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/run-with-concurrency.grit +6 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/translate-file-for-language.grit +6 -0
- package/src/migrations/latest/translate-script-whole-file-writes/metadata.json +3 -0
- package/src/migrations/latest/translate-script-whole-file-writes/migration.d.ts +2 -0
- package/src/migrations/latest/translate-script-whole-file-writes/migration.js +103 -0
- package/src/migrations/latest/translate-script-whole-file-writes/migration.js.map +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/test-fixtures/released-script-first.ts.fixture +430 -0
- package/src/migrations/latest/translate-script-whole-file-writes/test-fixtures/released-script.ts.fixture +431 -0
- package/src/preset/__snapshots__/generator.spec.ts.snap +2 -0
- package/src/ts/astro-docs/files/translation/scripts/translate.ts.template +269 -148
- package/src/utils/agent-core-constructs/agent-core-constructs.d.ts +25 -0
- package/src/utils/agent-core-constructs/agent-core-constructs.js +59 -0
- package/src/utils/agent-core-constructs/agent-core-constructs.js.map +1 -1
- package/src/utils/agent-core-constructs/files/cdk/app/agentcore-harness/__nameKebabCase__/__nameKebabCase__.ts.template +366 -0
- package/src/utils/agent-core-constructs/files/terraform/app/agentcore-harness/__nameKebabCase__/__nameKebabCase__.tf.template +282 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { addProjectConfiguration, generateFiles, joinPathFragments, OverwriteStrategy, updateProjectConfiguration } from "@nx/devkit";
|
|
5
|
+
import { addTsDependencies } from "../utils/add-dependencies.js";
|
|
6
|
+
import { addAgentCoreHarnessInfra } from "../utils/agent-core-constructs/agent-core-constructs.js";
|
|
7
|
+
import { declareDependencies, ownedElsewhere } from "../utils/declared-dependencies.js";
|
|
8
|
+
import { formatFilesInSubtree } from "../utils/format.js";
|
|
9
|
+
import { resolveIac } from "../utils/iac.js";
|
|
10
|
+
import { installDependencies } from "../utils/install.js";
|
|
11
|
+
import { addGeneratorMetricsIfApplicable } from "../utils/metrics.js";
|
|
12
|
+
import { kebabCase, toClassName } from "../utils/names.js";
|
|
13
|
+
import { getNpmScopePrefix } from "../utils/npm-scope.js";
|
|
14
|
+
import { addGeneratorMetadata, getGeneratorInfo, projectExists, readProjectConfigurationUnqualified } from "../utils/nx.js";
|
|
15
|
+
import { SHARED_CONSTRUCTS_DEPENDENCIES, sharedConstructsGenerator } from "../utils/shared-constructs.js";
|
|
16
|
+
// scripts/chat.ts needs all of these whatever the infrastructure choice, so no
|
|
17
|
+
// entry is conditional. The harness project holds no manifest of its own, so
|
|
18
|
+
// every entry is declared against the workspace root.
|
|
19
|
+
export const DEPENDENCIES = declareDependencies()({
|
|
20
|
+
ts: [
|
|
21
|
+
{
|
|
22
|
+
name: '@aws-sdk/client-bedrock-agentcore',
|
|
23
|
+
root: true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: '@aws-sdk/client-appconfigdata',
|
|
27
|
+
root: true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: '@aws-lambda-powertools/parameters',
|
|
31
|
+
root: true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'agent-chat-cli',
|
|
35
|
+
root: true
|
|
36
|
+
},
|
|
37
|
+
// tsx runs the chat script via the `chat` target; both are shared tooling.
|
|
38
|
+
{
|
|
39
|
+
name: '@types/node',
|
|
40
|
+
dev: true,
|
|
41
|
+
root: true
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'tsx',
|
|
45
|
+
dev: true,
|
|
46
|
+
root: true
|
|
47
|
+
},
|
|
48
|
+
...ownedElsewhere(SHARED_CONSTRUCTS_DEPENDENCIES)
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
export const AGENTCORE_HARNESS_GENERATOR_INFO = getGeneratorInfo(import.meta.filename);
|
|
52
|
+
export const agentcoreHarnessGenerator = async (tree, options)=>{
|
|
53
|
+
const nameKebabCase = kebabCase(options.name);
|
|
54
|
+
const nameClassName = toClassName(nameKebabCase);
|
|
55
|
+
const parentDir = options.directory ?? 'packages';
|
|
56
|
+
const subDir = options.subDirectory ?? nameKebabCase;
|
|
57
|
+
const projectRoot = joinPathFragments(parentDir, subDir);
|
|
58
|
+
const fullyQualifiedProjectName = `${getNpmScopePrefix(tree)}${nameKebabCase}`;
|
|
59
|
+
const infra = options.infra ?? 'agentcore';
|
|
60
|
+
// AgentCore harness names must start with an ASCII letter and stay within 40
|
|
61
|
+
// characters once the "_<8 hex>" suffix is appended, so cap the prefix at 31.
|
|
62
|
+
// toClassName prefixes `_` (not a letter) for digit-leading names.
|
|
63
|
+
const harnessNamePrefix = (/^[A-Za-z]/.test(nameClassName) ? nameClassName : `H${nameClassName}`).slice(0, 31);
|
|
64
|
+
// Reuse an existing project configuration; create it otherwise.
|
|
65
|
+
let project;
|
|
66
|
+
if (projectExists(tree, fullyQualifiedProjectName)) {
|
|
67
|
+
project = readProjectConfigurationUnqualified(tree, fullyQualifiedProjectName);
|
|
68
|
+
} else {
|
|
69
|
+
addProjectConfiguration(tree, fullyQualifiedProjectName, {
|
|
70
|
+
name: fullyQualifiedProjectName,
|
|
71
|
+
root: projectRoot,
|
|
72
|
+
projectType: 'application',
|
|
73
|
+
sourceRoot: projectRoot,
|
|
74
|
+
tags: [],
|
|
75
|
+
targets: {},
|
|
76
|
+
metadata: {
|
|
77
|
+
generator: AGENTCORE_HARNESS_GENERATOR_INFO.id
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
project = readProjectConfigurationUnqualified(tree, fullyQualifiedProjectName);
|
|
81
|
+
}
|
|
82
|
+
// `chat` runs the vended chat script against the deployed Harness. Re-run:
|
|
83
|
+
// keep an existing target so user edits survive.
|
|
84
|
+
project.targets ??= {};
|
|
85
|
+
project.targets['chat'] ??= {
|
|
86
|
+
executor: 'nx:run-commands',
|
|
87
|
+
options: {
|
|
88
|
+
commands: [
|
|
89
|
+
'tsx ./scripts/chat.ts'
|
|
90
|
+
],
|
|
91
|
+
cwd: '{projectRoot}'
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
updateProjectConfiguration(tree, project.name, project);
|
|
95
|
+
generateFiles(tree, joinPathFragments(import.meta.dirname, 'files', 'project'), projectRoot, {
|
|
96
|
+
nameClassName,
|
|
97
|
+
nameKebabCase,
|
|
98
|
+
fullyQualifiedName: fullyQualifiedProjectName
|
|
99
|
+
}, {
|
|
100
|
+
overwriteStrategy: OverwriteStrategy.KeepExisting
|
|
101
|
+
});
|
|
102
|
+
// Resolved before the metadata write because the recorded value is what the
|
|
103
|
+
// declaration's predicates read; undefined when no infrastructure is written,
|
|
104
|
+
// in which case neither provider's packages were added.
|
|
105
|
+
const iac = infra === 'agentcore' ? await resolveIac(tree, options.iac) : undefined;
|
|
106
|
+
// Record only what cannot drift once the user owns the generated
|
|
107
|
+
// infrastructure: the project identity, its runtime config key, its auth mode
|
|
108
|
+
// and which provider's infrastructure was written.
|
|
109
|
+
const metadata = {
|
|
110
|
+
name: nameKebabCase,
|
|
111
|
+
rc: nameClassName,
|
|
112
|
+
auth: 'iam',
|
|
113
|
+
...iac ? {
|
|
114
|
+
iac
|
|
115
|
+
} : {}
|
|
116
|
+
};
|
|
117
|
+
addGeneratorMetadata(tree, fullyQualifiedProjectName, AGENTCORE_HARNESS_GENERATOR_INFO, metadata);
|
|
118
|
+
// scripts/chat.ts dependencies; tsx runs it via the `chat` target.
|
|
119
|
+
addTsDependencies(tree, DEPENDENCIES, {
|
|
120
|
+
metadata
|
|
121
|
+
});
|
|
122
|
+
// Harness infrastructure is written only for `infra: agentcore`. A later
|
|
123
|
+
// `infra: agentcore` rerun adds it without replacing project files.
|
|
124
|
+
if (iac) {
|
|
125
|
+
await sharedConstructsGenerator(tree, {
|
|
126
|
+
iac
|
|
127
|
+
}, DEPENDENCIES);
|
|
128
|
+
await addAgentCoreHarnessInfra(tree, {
|
|
129
|
+
harnessNameClassName: nameClassName,
|
|
130
|
+
harnessNameKebabCase: nameKebabCase,
|
|
131
|
+
projectRoot,
|
|
132
|
+
harnessNamePrefix,
|
|
133
|
+
iac
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
await addGeneratorMetricsIfApplicable(tree, [
|
|
137
|
+
AGENTCORE_HARNESS_GENERATOR_INFO
|
|
138
|
+
]);
|
|
139
|
+
await formatFilesInSubtree(tree);
|
|
140
|
+
return ()=>installDependencies(tree, options.preferInstallDependencies, {
|
|
141
|
+
languages: [
|
|
142
|
+
'typescript'
|
|
143
|
+
]
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Read a Harness Project's metadata, validating it was generated by the
|
|
148
|
+
* agentcore-harness generator before returning typed data.
|
|
149
|
+
*/ export const readAgentCoreHarnessMetadata = (project)=>{
|
|
150
|
+
const metadata = project.metadata;
|
|
151
|
+
if (metadata?.generator !== AGENTCORE_HARNESS_GENERATOR_INFO.id) {
|
|
152
|
+
throw new Error(`Project '${project.name}' was not generated by the '${AGENTCORE_HARNESS_GENERATOR_INFO.id}' generator.`);
|
|
153
|
+
}
|
|
154
|
+
return metadata;
|
|
155
|
+
};
|
|
156
|
+
export default agentcoreHarnessGenerator;
|
|
157
|
+
|
|
158
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../packages/nx-plugin/src/agentcore-harness/generator.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n addProjectConfiguration,\n type GeneratorCallback,\n generateFiles,\n joinPathFragments,\n OverwriteStrategy,\n type ProjectConfiguration,\n type Tree,\n updateProjectConfiguration,\n} from '@nx/devkit';\nimport { addTsDependencies } from '../utils/add-dependencies';\nimport { addAgentCoreHarnessInfra } from '../utils/agent-core-constructs/agent-core-constructs';\nimport {\n declareDependencies,\n ownedElsewhere,\n} from '../utils/declared-dependencies';\nimport { formatFilesInSubtree } from '../utils/format';\nimport { resolveIac } from '../utils/iac';\nimport { installDependencies } from '../utils/install';\nimport { addGeneratorMetricsIfApplicable } from '../utils/metrics';\nimport { kebabCase, toClassName } from '../utils/names';\nimport { getNpmScopePrefix } from '../utils/npm-scope';\nimport {\n addGeneratorMetadata,\n getGeneratorInfo,\n type NxGeneratorInfo,\n projectExists,\n readProjectConfigurationUnqualified,\n} from '../utils/nx';\nimport {\n SHARED_CONSTRUCTS_DEPENDENCIES,\n sharedConstructsGenerator,\n} from '../utils/shared-constructs';\nimport type { IacMetadata } from '../utils/shared-constructs-constants';\nimport type { AgentcoreHarnessGeneratorSchema } from './schema';\n\n// scripts/chat.ts needs all of these whatever the infrastructure choice, so no\n// entry is conditional. The harness project holds no manifest of its own, so\n// every entry is declared against the workspace root.\nexport const DEPENDENCIES = declareDependencies<AgentCoreHarnessMetadata>()({\n ts: [\n { name: '@aws-sdk/client-bedrock-agentcore', root: true },\n { name: '@aws-sdk/client-appconfigdata', root: true },\n { name: '@aws-lambda-powertools/parameters', root: true },\n { name: 'agent-chat-cli', root: true },\n // tsx runs the chat script via the `chat` target; both are shared tooling.\n { name: '@types/node', dev: true, root: true },\n { name: 'tsx', dev: true, root: true },\n ...ownedElsewhere(SHARED_CONSTRUCTS_DEPENDENCIES),\n ],\n});\n\nexport const AGENTCORE_HARNESS_GENERATOR_INFO: NxGeneratorInfo =\n getGeneratorInfo(import.meta.filename);\n\nexport const agentcoreHarnessGenerator = async (\n tree: Tree,\n options: AgentcoreHarnessGeneratorSchema,\n): Promise<GeneratorCallback> => {\n const nameKebabCase = kebabCase(options.name);\n const nameClassName = toClassName(nameKebabCase);\n const parentDir = options.directory ?? 'packages';\n const subDir = options.subDirectory ?? nameKebabCase;\n const projectRoot = joinPathFragments(parentDir, subDir);\n const fullyQualifiedProjectName = `${getNpmScopePrefix(tree)}${nameKebabCase}`;\n const infra = options.infra ?? 'agentcore';\n\n // AgentCore harness names must start with an ASCII letter and stay within 40\n // characters once the \"_<8 hex>\" suffix is appended, so cap the prefix at 31.\n // toClassName prefixes `_` (not a letter) for digit-leading names.\n const harnessNamePrefix = (\n /^[A-Za-z]/.test(nameClassName) ? nameClassName : `H${nameClassName}`\n ).slice(0, 31);\n\n // Reuse an existing project configuration; create it otherwise.\n let project: ProjectConfiguration;\n if (projectExists(tree, fullyQualifiedProjectName)) {\n project = readProjectConfigurationUnqualified(\n tree,\n fullyQualifiedProjectName,\n );\n } else {\n addProjectConfiguration(tree, fullyQualifiedProjectName, {\n name: fullyQualifiedProjectName,\n root: projectRoot,\n projectType: 'application',\n sourceRoot: projectRoot,\n tags: [],\n targets: {},\n metadata: {\n generator: AGENTCORE_HARNESS_GENERATOR_INFO.id,\n } as any,\n });\n project = readProjectConfigurationUnqualified(\n tree,\n fullyQualifiedProjectName,\n );\n }\n\n // `chat` runs the vended chat script against the deployed Harness. Re-run:\n // keep an existing target so user edits survive.\n project.targets ??= {};\n project.targets['chat'] ??= {\n executor: 'nx:run-commands',\n options: {\n commands: ['tsx ./scripts/chat.ts'],\n cwd: '{projectRoot}',\n },\n };\n updateProjectConfiguration(tree, project.name, project);\n\n generateFiles(\n tree,\n joinPathFragments(import.meta.dirname, 'files', 'project'),\n projectRoot,\n {\n nameClassName,\n nameKebabCase,\n fullyQualifiedName: fullyQualifiedProjectName,\n },\n { overwriteStrategy: OverwriteStrategy.KeepExisting },\n );\n\n // Resolved before the metadata write because the recorded value is what the\n // declaration's predicates read; undefined when no infrastructure is written,\n // in which case neither provider's packages were added.\n const iac =\n infra === 'agentcore' ? await resolveIac(tree, options.iac) : undefined;\n\n // Record only what cannot drift once the user owns the generated\n // infrastructure: the project identity, its runtime config key, its auth mode\n // and which provider's infrastructure was written.\n const metadata: Omit<AgentCoreHarnessMetadata, 'generator'> = {\n name: nameKebabCase,\n rc: nameClassName,\n auth: 'iam',\n ...(iac ? { iac } : {}),\n };\n\n addGeneratorMetadata(\n tree,\n fullyQualifiedProjectName,\n AGENTCORE_HARNESS_GENERATOR_INFO,\n metadata,\n );\n\n // scripts/chat.ts dependencies; tsx runs it via the `chat` target.\n addTsDependencies(tree, DEPENDENCIES, { metadata });\n\n // Harness infrastructure is written only for `infra: agentcore`. A later\n // `infra: agentcore` rerun adds it without replacing project files.\n if (iac) {\n await sharedConstructsGenerator(tree, { iac }, DEPENDENCIES);\n await addAgentCoreHarnessInfra(tree, {\n harnessNameClassName: nameClassName,\n harnessNameKebabCase: nameKebabCase,\n projectRoot,\n harnessNamePrefix,\n iac,\n });\n }\n\n await addGeneratorMetricsIfApplicable(tree, [\n AGENTCORE_HARNESS_GENERATOR_INFO,\n ]);\n\n await formatFilesInSubtree(tree);\n return () =>\n installDependencies(tree, options.preferInstallDependencies, {\n languages: ['typescript'],\n });\n};\n\n/**\n * Harness details stored in the Harness Project's metadata. `iac` records which\n * provider's infrastructure was written, so the version sync can tell which\n * shared constructs packages this project owns.\n */\nexport interface AgentCoreHarnessMetadata extends IacMetadata {\n generator: string;\n name: string;\n rc: string;\n auth: 'iam';\n}\n\n/**\n * Read a Harness Project's metadata, validating it was generated by the\n * agentcore-harness generator before returning typed data.\n */\nexport const readAgentCoreHarnessMetadata = (\n project: ProjectConfiguration,\n): AgentCoreHarnessMetadata => {\n const metadata = project.metadata as any;\n if (metadata?.generator !== AGENTCORE_HARNESS_GENERATOR_INFO.id) {\n throw new Error(\n `Project '${project.name}' was not generated by the '${AGENTCORE_HARNESS_GENERATOR_INFO.id}' generator.`,\n );\n }\n return metadata as AgentCoreHarnessMetadata;\n};\n\nexport default agentcoreHarnessGenerator;\n"],"names":["addProjectConfiguration","generateFiles","joinPathFragments","OverwriteStrategy","updateProjectConfiguration","addTsDependencies","addAgentCoreHarnessInfra","declareDependencies","ownedElsewhere","formatFilesInSubtree","resolveIac","installDependencies","addGeneratorMetricsIfApplicable","kebabCase","toClassName","getNpmScopePrefix","addGeneratorMetadata","getGeneratorInfo","projectExists","readProjectConfigurationUnqualified","SHARED_CONSTRUCTS_DEPENDENCIES","sharedConstructsGenerator","DEPENDENCIES","ts","name","root","dev","AGENTCORE_HARNESS_GENERATOR_INFO","filename","agentcoreHarnessGenerator","tree","options","nameKebabCase","nameClassName","parentDir","directory","subDir","subDirectory","projectRoot","fullyQualifiedProjectName","infra","harnessNamePrefix","test","slice","project","projectType","sourceRoot","tags","targets","metadata","generator","id","executor","commands","cwd","dirname","fullyQualifiedName","overwriteStrategy","KeepExisting","iac","undefined","rc","auth","harnessNameClassName","harnessNameKebabCase","preferInstallDependencies","languages","readAgentCoreHarnessMetadata","Error"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../packages/nx-plugin/src/agentcore-harness/schema.d.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { IacOption } from '../utils/iac';\n\n/**\n * Options for the agentcore-harness generator, mirroring schema.json.\n */\nexport interface AgentcoreHarnessGeneratorSchema {\n name: string;\n directory?: string;\n subDirectory?: string;\n infra?: 'agentcore' | 'none';\n iac?: IacOption;\n preferInstallDependencies?: boolean;\n}\n"],"names":[],"mappings":"AAAA;;;CAGC,GAGD;;CAEC,GACD,WAOC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import type { IacOption } from '../utils/iac';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for the agentcore-harness generator, mirroring schema.json.
|
|
9
|
+
*/
|
|
10
|
+
export interface AgentcoreHarnessGeneratorSchema {
|
|
11
|
+
name: string;
|
|
12
|
+
directory?: string;
|
|
13
|
+
subDirectory?: string;
|
|
14
|
+
infra?: 'agentcore' | 'none';
|
|
15
|
+
iac?: IacOption;
|
|
16
|
+
preferInstallDependencies?: boolean;
|
|
17
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"$id": "agentcore-harness",
|
|
4
|
+
"title": "agentcore-harness",
|
|
5
|
+
"description": "Generate an Amazon Bedrock AgentCore Harness project",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"name": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "The name of your AgentCore Harness project. Must contain at least one non-whitespace character which can be normalized into a kebab-case project name (eg. my-harness).",
|
|
11
|
+
"$default": {
|
|
12
|
+
"$source": "argv",
|
|
13
|
+
"index": 0
|
|
14
|
+
},
|
|
15
|
+
"x-prompt": "What would you like to call your AgentCore Harness?",
|
|
16
|
+
"x-priority": "important",
|
|
17
|
+
"pattern": "\\S"
|
|
18
|
+
},
|
|
19
|
+
"directory": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Parent directory where the harness project is placed. Defaults to packages. Must be a relative path which does not contain parent directory (..) segments.",
|
|
22
|
+
"x-priority": "important",
|
|
23
|
+
"pattern": "^(?![A-Za-z]:)(?![\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$))(?=.*\\S).+$"
|
|
24
|
+
},
|
|
25
|
+
"subDirectory": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "The sub directory the project is placed in. Defaults to the kebab-case harness name. Must be a relative path which does not contain parent directory (..) segments.",
|
|
28
|
+
"x-priority": "important",
|
|
29
|
+
"pattern": "^(?![A-Za-z]:)(?![\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$))(?=.*\\S).+$"
|
|
30
|
+
},
|
|
31
|
+
"infra": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "The type of infrastructure to generate for hosting your harness. Defaults to agentcore. Select none for no hosting.",
|
|
34
|
+
"x-prompt": "How would you like to host your harness?",
|
|
35
|
+
"x-priority": "important",
|
|
36
|
+
"enum": ["agentcore", "none"],
|
|
37
|
+
"default": "agentcore"
|
|
38
|
+
},
|
|
39
|
+
"iac": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "The preferred IaC provider for generated harness infrastructure. Defaults to inherit, which uses the provider configured for your workspace.",
|
|
42
|
+
"x-prompt": "Which provider would you like to manage your infrastructure? (default: inherit)",
|
|
43
|
+
"x-priority": "important",
|
|
44
|
+
"enum": ["inherit", "cdk", "terraform"],
|
|
45
|
+
"default": "inherit"
|
|
46
|
+
},
|
|
47
|
+
"preferInstallDependencies": {
|
|
48
|
+
"type": "boolean",
|
|
49
|
+
"description": "Whether to prefer installing dependencies after the generator runs. Defaults to true. Set to false to defer installing when batching multiple generators (an install still runs if needed so subsequent generators can compute the Nx project graph); install once at the end."
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"required": ["name"]
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function buildSystemPrompt(targetLang: string): string {
|
|
2
|
+
return `You are an expert technical-documentation translator. You translate MDX documentation files from the source locale \`${config.sourceLanguage}\` into the target locale \`${targetLang}\`.
|
|
3
|
+
|
|
4
|
+
Both are locale codes (ISO 639-1 / BCP-47 style, or short forms like \`jp\`, \`zh\`, \`pt\`). Translate naturally into the language the target code identifies.
|
|
5
|
+
|
|
6
|
+
Use the \`fileEditor\` tool to read the files you are given and to write your translation. Read a long file in slices with \`view_range\`. Use the absolute paths exactly as given — write to the target path verbatim, never to a variation of it.
|
|
7
|
+
|
|
8
|
+
Translate the prose. Everything else is copied from the source exactly as it is:
|
|
9
|
+
|
|
10
|
+
- Code blocks. Reproduce each one character for character: the language and \`title\` on the opening fence, and every line of the body. Comments inside code are code — a \`# Creates a new subsegment\` stays in English. Never translate anything between the fences.
|
|
11
|
+
- Inline code, URLs, link paths, \`import\` statements, component names, JSX attributes, and \`<Snippet name="..." />\` values.
|
|
12
|
+
- Product, service, and people's names, and package names such as \`@aws/nx-plugin\` — translate no part of them, even where they appear in prose or in a \`title\`.
|
|
13
|
+
- The MDX structure itself: frontmatter delimiters, heading levels, lists, and the blank lines between blocks. Add none and remove none.
|
|
14
|
+
|
|
15
|
+
Two things do change:
|
|
16
|
+
|
|
17
|
+
- In frontmatter, translate only \`title\` and \`description\`. Leave every other key and value alone. Frontmatter is YAML, so wrap a value in double quotes whenever it would otherwise begin with a character YAML reserves — for example a title starting with \`@aws/nx-plugin\` must be written as \`"@aws/nx-plugin ..."\`.
|
|
18
|
+
- Rewrite link paths that embed the source locale (\`/${config.sourceLanguage}/foo\` becomes \`/${targetLang}/foo\`), and make a \`parentHeading\` match the translated heading it sits under.
|
|
19
|
+
|
|
20
|
+
Write the finished file in one \`create\` call, and never with \`str_replace\` — a partial replacement silently duplicates or splices sections. Do not wrap the file in triple backticks.
|
|
21
|
+
|
|
22
|
+
When the file is written, reply with a one-line summary and stop.`;
|
|
23
|
+
}
|
package/src/migrations/latest/translate-script-whole-file-writes/files/build-user-prompt.ts.fixture
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
function buildUserPrompt(file: FileToTranslate, targetLang: string): string {
|
|
2
|
+
const targetAbsPath = path.join(DOCS_DIR, targetLang, file.relativePath);
|
|
3
|
+
|
|
4
|
+
if (!file.diffPath) {
|
|
5
|
+
return `Translate \`${file.sourceAbsPath}\` into locale \`${targetLang}\` and write it to \`${targetAbsPath}\`.
|
|
6
|
+
|
|
7
|
+
Read the source, then write the whole translated file with \`create\`. Always write it, even if a translation is already there — it may be out of date.`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return `Update the \`${targetLang}\` translation of \`${file.sourceAbsPath}\`.
|
|
11
|
+
|
|
12
|
+
- Source: \`${file.sourceAbsPath}\`
|
|
13
|
+
- Existing translation to update: \`${targetAbsPath}\`
|
|
14
|
+
- Diff of what changed in the source: \`${file.diffPath}\`
|
|
15
|
+
|
|
16
|
+
Read all three. Retranslate only what the diff touched, keep the existing translated wording everywhere else, then write the whole file back with \`create\`.`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const FENCE_RE = /^\s*(?:`{3,}|~{3,})/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Split a document into alternating prose and fenced-code segments.
|
|
23
|
+
*/
|
|
24
|
+
function splitOnFences(text: string): { prose: string[]; code: string[] } {
|
|
25
|
+
const prose: string[] = [];
|
|
26
|
+
const code: string[] = [];
|
|
27
|
+
let current: string[] = [];
|
|
28
|
+
let inFence = false;
|
|
29
|
+
for (const line of text.split('\n')) {
|
|
30
|
+
if (FENCE_RE.test(line)) {
|
|
31
|
+
(inFence ? code : prose).push(current.concat(line).join('\n'));
|
|
32
|
+
current = [];
|
|
33
|
+
inFence = !inFence;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
current.push(line);
|
|
37
|
+
}
|
|
38
|
+
(inFence ? code : prose).push(current.join('\n'));
|
|
39
|
+
return { prose, code };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* YAML frontmatter must still parse after translation. A value that begins with
|
|
44
|
+
* a character YAML reserves — most often a title starting with \`@aws/...\` —
|
|
45
|
+
* breaks the docs build, so reject it here and let the retry quote it.
|
|
46
|
+
*/
|
|
47
|
+
function assertFrontmatterIsParseable(text: string): void {
|
|
48
|
+
const lines = text.split('\n');
|
|
49
|
+
if (lines[0]?.trim() !== '---') return;
|
|
50
|
+
for (let i = 1; i < lines.length && lines[i].trim() !== '---'; i++) {
|
|
51
|
+
const value = lines[i].match(/^[A-Za-z0-9_-]+:\s+(\S)/)?.[1];
|
|
52
|
+
if (value && '@*&%!|>'.includes(value)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`frontmatter line ${i + 1} starts with the reserved YAML character "${value}" and must be quoted: ${lines[i].trim()}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Copy the source's code blocks over the translated file's. The prompt tells the
|
|
62
|
+
* agent to reproduce them verbatim, but it occasionally translates a comment
|
|
63
|
+
* anyway, which would ship stale or broken code.
|
|
64
|
+
*
|
|
65
|
+
* A differing block count means the translation lost the document's shape —
|
|
66
|
+
* usually a section duplicated or dropped — so this throws rather than stitching
|
|
67
|
+
* mismatched blocks together, letting the caller retry.
|
|
68
|
+
*/
|
|
69
|
+
async function restoreCodeBlocks(
|
|
70
|
+
sourceAbsPath: string,
|
|
71
|
+
targetAbsPath: string,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
const source = splitOnFences(await fs.readFile(sourceAbsPath, 'utf-8'));
|
|
74
|
+
const target = splitOnFences(await fs.readFile(targetAbsPath, 'utf-8'));
|
|
75
|
+
|
|
76
|
+
if (source.code.length !== target.code.length) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`translation has ${target.code.length} code block(s) but the source has ${source.code.length}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (source.code.length === 0) return;
|
|
82
|
+
|
|
83
|
+
const merged = target.prose.flatMap((chunk, i) =>
|
|
84
|
+
i < source.code.length ? [chunk, source.code[i]] : [chunk],
|
|
85
|
+
);
|
|
86
|
+
await fs.writeFile(targetAbsPath, merged.join('\n'), 'utf-8');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Number of times to retry a single (file x language) translation. Large files
|
|
91
|
+
* occasionally hit transient Bedrock streaming errors; a fresh agent invocation
|
|
92
|
+
* re-reads the files from disk, so retries are idempotent.
|
|
93
|
+
*/
|
|
94
|
+
const TRANSLATE_MAX_ATTEMPTS = 4;
|
|
95
|
+
|
|
96
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const CONFIG_PATH = path.resolve(SCRIPTS_DIR, 'translate.config.json');
|
package/src/migrations/latest/translate-script-whole-file-writes/files/file-to-translate.ts.fixture
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface FileToTranslate {
|
|
2
|
+
relativePath: string;
|
|
3
|
+
sourceAbsPath: string;
|
|
4
|
+
/**
|
|
5
|
+
* Path to a file holding the source diff, or undefined when there is no prior
|
|
6
|
+
* translation to update. Passed to the agent as a path rather than inlined
|
|
7
|
+
* into the prompt, so it reads only as much of it as it needs.
|
|
8
|
+
*/
|
|
9
|
+
diffPath?: string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
async function getFilesToTranslate(): Promise<FileToTranslate[]> {
|
|
2
|
+
const sourceLangRoot = `${DOCS_DIR}/${config.sourceLanguage}`;
|
|
3
|
+
const includePatterns = config.include.map((p) => `${sourceLangRoot}/${p}`);
|
|
4
|
+
const ignorePatterns = config.exclude.map((p) => `${sourceLangRoot}/${p}`);
|
|
5
|
+
|
|
6
|
+
if (options.all) {
|
|
7
|
+
log.info('Translating all source documentation files');
|
|
8
|
+
const files = await glob(includePatterns, { ignore: ignorePatterns });
|
|
9
|
+
return files.map((file) => ({
|
|
10
|
+
relativePath: path.relative(sourceLangRoot, file),
|
|
11
|
+
sourceAbsPath: file,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const git = simpleGit();
|
|
16
|
+
// git reports paths relative to the repo root, which is not necessarily the
|
|
17
|
+
// directory this script runs from.
|
|
18
|
+
const repoRoot = (await git.revparse(['--show-toplevel'])).trim();
|
|
19
|
+
|
|
20
|
+
let currentBranch: string;
|
|
21
|
+
let mainBranch: string;
|
|
22
|
+
if (process.env.GITHUB_HEAD_REF) {
|
|
23
|
+
currentBranch = `origin/${process.env.GITHUB_HEAD_REF}`;
|
|
24
|
+
mainBranch = 'origin/main';
|
|
25
|
+
} else {
|
|
26
|
+
currentBranch = (await git.branch()).current;
|
|
27
|
+
mainBranch = 'main';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await git.raw(['rev-parse', '--verify', mainBranch]);
|
|
32
|
+
} catch {
|
|
33
|
+
log.warn(`Could not find "${mainBranch}"; falling back to --all behaviour`);
|
|
34
|
+
options.all = true;
|
|
35
|
+
return getFilesToTranslate();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mergeBase = (
|
|
39
|
+
await git.raw(['merge-base', mainBranch, currentBranch])
|
|
40
|
+
).trim();
|
|
41
|
+
|
|
42
|
+
const translationCommits = (
|
|
43
|
+
await git.log({ from: mergeBase, to: 'HEAD' })
|
|
44
|
+
).all.filter((c) => c.message.includes(TRANSLATION_COMMIT_MESSAGE));
|
|
45
|
+
|
|
46
|
+
const baseCommit =
|
|
47
|
+
translationCommits.length > 0 ? translationCommits[0].hash : mergeBase;
|
|
48
|
+
|
|
49
|
+
log.info(
|
|
50
|
+
translationCommits.length > 0
|
|
51
|
+
? `Detecting changed files since last translation commit ${baseCommit.substring(0, 7)}`
|
|
52
|
+
: `Detecting changed files since branch creation ${baseCommit.substring(0, 7)}`,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const diffNames = (
|
|
56
|
+
await git.diff([`${baseCommit}..HEAD`, '--name-only', '--diff-filter=d'])
|
|
57
|
+
)
|
|
58
|
+
.split('\n')
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
|
|
61
|
+
const { files: uncommitted } = await git.status();
|
|
62
|
+
const uncommittedNames = uncommitted.map((f) => f.path);
|
|
63
|
+
|
|
64
|
+
const allCandidates = [...new Set([...diffNames, ...uncommittedNames])].map(
|
|
65
|
+
(p) => path.resolve(repoRoot, p),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Filter to files inside the source language dir that match include/exclude
|
|
69
|
+
const includedGlob = await glob(includePatterns, {
|
|
70
|
+
ignore: ignorePatterns,
|
|
71
|
+
});
|
|
72
|
+
const includedSet = new Set(includedGlob.map((p) => path.resolve(p)));
|
|
73
|
+
|
|
74
|
+
const changed = allCandidates.filter((abs) => includedSet.has(abs));
|
|
75
|
+
|
|
76
|
+
if (changed.length === 0) {
|
|
77
|
+
log.warn('No changed source documentation files detected');
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return Promise.all(
|
|
82
|
+
changed.map(async (file) => {
|
|
83
|
+
const relativePath = path.relative(sourceLangRoot, file);
|
|
84
|
+
const diff = await git.diff([
|
|
85
|
+
`${baseCommit}..HEAD`,
|
|
86
|
+
'--',
|
|
87
|
+
path.relative(repoRoot, file),
|
|
88
|
+
]);
|
|
89
|
+
return {
|
|
90
|
+
relativePath,
|
|
91
|
+
sourceAbsPath: file,
|
|
92
|
+
diffPath: diff ? await writeDiff(relativePath, diff) : undefined,
|
|
93
|
+
};
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate documentation files using a Strands agent powered by Claude on
|
|
3
|
+
* Amazon Bedrock.
|
|
4
|
+
*
|
|
5
|
+
* Configuration lives in ./translate.config.json (sibling to this file).
|
|
6
|
+
* Run from the project root:
|
|
7
|
+
*
|
|
8
|
+
* <%= pkgMgrCmd %> nx translate <%= fullyQualifiedName %> -- --all
|
|
9
|
+
* <%= pkgMgrCmd %> nx translate <%= fullyQualifiedName %> # only files changed since last translate commit
|
|
10
|
+
*
|
|
11
|
+
* The driver works out which source docs to translate and spawns one Strands
|
|
12
|
+
* agent per (file × target language). Each agent gets the `fileEditor` tool and
|
|
13
|
+
* the paths it needs — the source file, the target file, and a diff of what
|
|
14
|
+
* changed — then reads, translates and writes the file itself.
|
|
15
|
+
*/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const log = {
|
|
2
|
+
info: (m: string) => console.log(`[translate] ${m}`),
|
|
3
|
+
warn: (m: string) => console.warn(`[translate] ${m}`),
|
|
4
|
+
error: (m: string) => console.error(`[translate] ERROR ${m}`),
|
|
5
|
+
verbose: (m: string) => options.verbose && console.log(`[translate] ${m}`),
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Diffs live under the docs directory so the agent's file access — which is
|
|
10
|
+
* scoped to that directory — can read them. Removed when the run finishes.
|
|
11
|
+
*/
|
|
12
|
+
const DIFF_DIR = path.join(DOCS_DIR, '.translate-diffs');
|
|
13
|
+
|
|
14
|
+
async function writeDiff(relativePath: string, diff: string): Promise<string> {
|
|
15
|
+
const diffPath = path.join(DIFF_DIR, `${relativePath}.diff`);
|
|
16
|
+
await fs.outputFile(diffPath, diff, 'utf-8');
|
|
17
|
+
return diffPath;
|
|
18
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
async function main() {
|
|
2
|
+
const requestedLanguages: string[] = options.languages
|
|
3
|
+
? options.languages
|
|
4
|
+
.split(',')
|
|
5
|
+
.map((l: string) => l.trim())
|
|
6
|
+
.filter(Boolean)
|
|
7
|
+
: config.targetLanguages;
|
|
8
|
+
|
|
9
|
+
const targetLanguages = requestedLanguages.filter(
|
|
10
|
+
(l) => l !== config.sourceLanguage,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
if (targetLanguages.length === 0) {
|
|
14
|
+
log.error('No target languages configured');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
log.info(`Source: ${config.sourceLanguage}`);
|
|
19
|
+
log.info(`Targets: ${targetLanguages.join(', ')}`);
|
|
20
|
+
|
|
21
|
+
await pruneOrphanedTranslations(targetLanguages);
|
|
22
|
+
|
|
23
|
+
const files = await getFilesToTranslate();
|
|
24
|
+
|
|
25
|
+
if (files.length === 0) {
|
|
26
|
+
log.info('No documentation files to translate.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
log.info(`Files to translate: ${files.length}`);
|
|
31
|
+
for (const f of files) {
|
|
32
|
+
log.verbose(
|
|
33
|
+
` - ${f.relativePath}${f.diffPath ? ' (changed)' : ' (full)'}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (options.dryRun) {
|
|
38
|
+
for (const lang of targetLanguages) {
|
|
39
|
+
for (const f of files) {
|
|
40
|
+
log.info(`[dry-run] would translate ${f.relativePath} → ${lang}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
log.info('Done (dry-run).');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Build one task per (file × target language) — each is a fresh agent invocation,
|
|
48
|
+
// so context windows stay small no matter how big the docs site is. A single
|
|
49
|
+
// failing translation records its error rather than aborting the whole run, so
|
|
50
|
+
// every other (file × language) still completes and gets written to disk.
|
|
51
|
+
const failures: string[] = [];
|
|
52
|
+
const tasks = targetLanguages.flatMap((lang) =>
|
|
53
|
+
files.map((file) => async () => {
|
|
54
|
+
log.info(` ${file.relativePath} → ${lang}`);
|
|
55
|
+
try {
|
|
56
|
+
await translateFileForLanguage(file, lang);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
log.error(
|
|
59
|
+
`${file.relativePath} → ${lang}: ${err instanceof Error ? err.message : String(err)}`,
|
|
60
|
+
);
|
|
61
|
+
failures.push(`${file.relativePath} → ${lang}`);
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const concurrency = Math.max(1, config.concurrency ?? 5);
|
|
67
|
+
log.info(
|
|
68
|
+
`Running ${tasks.length} translation(s) with concurrency=${concurrency}`,
|
|
69
|
+
);
|
|
70
|
+
await runWithConcurrency(tasks, concurrency);
|
|
71
|
+
|
|
72
|
+
if (failures.length > 0) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`${failures.length} translation(s) failed after retries:\n ${failures.join('\n ')}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
log.info('Done.');
|
|
79
|
+
}
|
package/src/migrations/latest/translate-script-whole-file-writes/files/project-root.ts.fixture
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const PROJECT_ROOT = path.resolve(SCRIPTS_DIR, '..');
|