Skip to content

Commit 75788f4

Browse files
committed
feat: introduce knock init and knock.json file
1 parent 6a8bd21 commit 75788f4

File tree

27 files changed

+688
-42
lines changed

27 files changed

+688
-42
lines changed

src/commands/guide/push.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default class GuidePush extends BaseCommand<typeof GuidePush> {
5353
const target = await Guide.ensureValidCommandTarget(
5454
this.props,
5555
this.runContext,
56+
this.projectConfig,
5657
);
5758

5859
const [guides, readErrors] = await Guide.readAllForCommandTarget(target, {

src/commands/guide/validate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default class GuideValidate extends BaseCommand<typeof GuideValidate> {
4141
const target = await Guide.ensureValidCommandTarget(
4242
this.props,
4343
this.runContext,
44+
this.projectConfig,
4445
);
4546

4647
const [guides, readErrors] = await Guide.readAllForCommandTarget(target, {

src/commands/init.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as path from "node:path";
2+
3+
import enquirer from "enquirer";
4+
import * as fs from "fs-extra";
5+
6+
import BaseCommand from "@/lib/base-command";
7+
import {
8+
PROJECT_CONFIG_FILE_NAME,
9+
ResourceDirectoriesByType,
10+
} from "@/lib/helpers/project-config";
11+
12+
export default class Init extends BaseCommand<typeof Init> {
13+
protected requiresAuth = false;
14+
15+
static summary =
16+
"Initialize a new Knock project with a knock.json configuration file.";
17+
18+
static description =
19+
"Creates a knock.json configuration file in the current directory " +
20+
"to store project-level settings like the knock resources directory.";
21+
22+
public async run(): Promise<void> {
23+
const configPath = path.resolve(process.cwd(), PROJECT_CONFIG_FILE_NAME);
24+
25+
// 1. Check if knock.json already exists
26+
const configExists = await fs.pathExists(configPath);
27+
if (configExists) {
28+
this.error(
29+
`A ${PROJECT_CONFIG_FILE_NAME} file already exists in this directory. Aborting.`,
30+
);
31+
}
32+
33+
// 2. Prompt user for the knock directory location
34+
const { knockDir } = await enquirer.prompt<{ knockDir: string }>({
35+
type: "input",
36+
name: "knockDir",
37+
message: "Where do you want to store your Knock resources?",
38+
initial: ".knock",
39+
});
40+
41+
// 3. Create the knock directory and resource subdirectories
42+
const knockDirPath = path.resolve(process.cwd(), knockDir);
43+
await fs.ensureDir(knockDirPath);
44+
45+
// Create resource subdirectories with .gitignore files
46+
for (const resourceDir of Object.values(ResourceDirectoriesByType)) {
47+
const resourceDirPath = path.resolve(knockDirPath, resourceDir);
48+
49+
// eslint-disable-next-line no-await-in-loop
50+
await fs.ensureDir(resourceDirPath);
51+
52+
// Create a .gitignore file in each resource directory
53+
const gitignorePath = path.resolve(resourceDirPath, ".gitignore");
54+
55+
// eslint-disable-next-line no-await-in-loop
56+
await fs.writeFile(gitignorePath, "");
57+
}
58+
59+
// 4. Write the knock.json configuration file
60+
await fs.outputJson(
61+
configPath,
62+
{
63+
$schema: "https://schemas.knock.app/cli/knock.json",
64+
knockDir,
65+
},
66+
{ spaces: 2 },
67+
);
68+
69+
this.log(`‣ Successfully initialized Knock project at ${process.cwd()}`);
70+
this.log(`‣ Resources directory: ${knockDir}`);
71+
}
72+
}

src/commands/layout/push.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export default class EmailLayoutPush extends BaseCommand<
5959
const target = await EmailLayout.ensureValidCommandTarget(
6060
this.props,
6161
this.runContext,
62+
this.projectConfig,
6263
);
6364

6465
const [layouts, readErrors] = await EmailLayout.readAllForCommandTarget(

src/commands/layout/validate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default class EmailLayoutValidate extends BaseCommand<
4747
const target = await EmailLayout.ensureValidCommandTarget(
4848
this.props,
4949
this.runContext,
50+
this.projectConfig,
5051
);
5152

5253
const [layouts, readErrors] = await EmailLayout.readAllForCommandTarget(

src/commands/message-type/push.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export default class MessageTypePush extends BaseCommand<
5858
const target = await MessageType.ensureValidCommandTarget(
5959
this.props,
6060
this.runContext,
61+
this.projectConfig,
6162
);
6263
const [messageTypes, readErrors] =
6364
await MessageType.readAllForCommandTarget(target, {

src/commands/message-type/validate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default class MessageTypeValidate extends BaseCommand<
4949
const target = await MessageType.ensureValidCommandTarget(
5050
this.props,
5151
this.runContext,
52+
this.projectConfig,
5253
);
5354

5455
const [messageTypes, readErrors] =

src/commands/partial/push.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default class PartialPush extends BaseCommand<typeof PartialPush> {
5656
const target = await Partial.ensureValidCommandTarget(
5757
this.props,
5858
this.runContext,
59+
this.projectConfig,
5960
);
6061

6162
const [partials, readErrors] = await Partial.readAllForCommandTarget(

src/commands/partial/validate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default class PartialValidate extends BaseCommand<
4646
const target = await Partial.ensureValidCommandTarget(
4747
this.props,
4848
this.runContext,
49+
this.projectConfig,
4950
);
5051

5152
const [partials, readErrors] = await Partial.readAllForCommandTarget(

src/commands/pull.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Flags } from "@oclif/core";
55
import BaseCommand from "@/lib/base-command";
66
import * as CustomFlags from "@/lib/helpers/flag";
77
import { DirContext } from "@/lib/helpers/fs";
8+
import { resolveKnockDir } from "@/lib/helpers/project-config";
89
import { promptToConfirm } from "@/lib/helpers/ux";
910
import {
1011
ALL_RESOURCE_TYPES,
@@ -31,7 +32,7 @@ export default class Pull extends BaseCommand<typeof Pull> {
3132
branch: CustomFlags.branch,
3233
"knock-dir": CustomFlags.dirPath({
3334
summary: "The target directory path to pull all resources into.",
34-
required: true,
35+
required: false,
3536
}),
3637
"hide-uncommitted-changes": Flags.boolean({
3738
summary: "Hide any uncommitted changes.",
@@ -43,7 +44,18 @@ export default class Pull extends BaseCommand<typeof Pull> {
4344

4445
public async run(): Promise<void> {
4546
const { flags } = this.props;
46-
const targetDirCtx = flags["knock-dir"];
47+
48+
// Resolve knock directory: flag takes precedence, otherwise use knock.json
49+
const targetDirCtx = await resolveKnockDir(
50+
flags["knock-dir"],
51+
this.projectConfig,
52+
);
53+
54+
if (!targetDirCtx) {
55+
this.error(
56+
"No knock directory specified. Either provide --knock-dir flag or run `knock init` to create a knock.json configuration file.",
57+
);
58+
}
4759

4860
const prompt = targetDirCtx.exists
4961
? `Pull latest resources into ${targetDirCtx.abspath}?\n This will overwrite the contents of this directory.`

0 commit comments

Comments
 (0)