Skip to content

Commit b80d410

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

File tree

27 files changed

+711
-36
lines changed

27 files changed

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

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: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import * as path from "node:path";
22

33
import { Flags } from "@oclif/core";
4+
import * as fs from "fs-extra";
45

56
import BaseCommand from "@/lib/base-command";
67
import * as CustomFlags from "@/lib/helpers/flag";
78
import { DirContext } from "@/lib/helpers/fs";
9+
import { resolveKnockDir } from "@/lib/helpers/project-config";
810
import { promptToConfirm } from "@/lib/helpers/ux";
911
import {
1012
ALL_RESOURCE_TYPES,
@@ -31,7 +33,7 @@ export default class Pull extends BaseCommand<typeof Pull> {
3133
branch: CustomFlags.branch,
3234
"knock-dir": CustomFlags.dirPath({
3335
summary: "The target directory path to pull all resources into.",
34-
required: true,
36+
required: false,
3537
}),
3638
"hide-uncommitted-changes": Flags.boolean({
3739
summary: "Hide any uncommitted changes.",
@@ -43,7 +45,30 @@ export default class Pull extends BaseCommand<typeof Pull> {
4345

4446
public async run(): Promise<void> {
4547
const { flags } = this.props;
46-
const targetDirCtx = flags["knock-dir"];
48+
49+
// Resolve knock directory: flag takes precedence, otherwise use knock.json
50+
const knockDirPath = resolveKnockDir(
51+
flags["knock-dir"]?.abspath,
52+
this.projectConfig,
53+
);
54+
55+
if (!knockDirPath) {
56+
this.error(
57+
"No knock directory specified. Either provide --knock-dir flag or run `knock init` to create a knock.json configuration file.",
58+
);
59+
}
60+
61+
// Convert knockDirPath to DirContext
62+
const abspath = path.isAbsolute(knockDirPath)
63+
? knockDirPath
64+
: path.resolve(process.cwd(), knockDirPath);
65+
66+
const exists = await fs.pathExists(abspath);
67+
if (exists && !(await fs.lstat(abspath)).isDirectory()) {
68+
this.error(`${knockDirPath} exists but is not a directory`);
69+
}
70+
71+
const targetDirCtx: DirContext = { abspath, exists };
4772

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

0 commit comments

Comments
 (0)